Files
NativeScript/packages/core/ui/styling/background.ios.ts

936 lines
33 KiB
TypeScript

import { ScrollEventData } from '../scroll-view';
import { CoreTypes } from '../../core-types';
import { Background as BackgroundDefinition } from './background';
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';
import { Length } from './style-properties';
import { BackgroundClearFlags } from './background-common';
export * from './background-common';
interface Rect {
left: number;
top: number;
right: number;
bottom: number;
}
const clearCGColor = UIColor.clearColor.CGColor;
const symbolUrl = Symbol('backgroundImageUrl');
export enum CacheMode {
none,
}
export namespace ios {
export function createBackgroundUIColor(view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const background = view.style.backgroundInternal;
const nativeView = <NativeScriptUIView>view.nativeViewProtected;
if (!nativeView) {
return;
}
if (background.clearFlags & BackgroundClearFlags.CLEAR_BOX_SHADOW) {
// clear box shadow if it has been removed!
view.setProperty('clipToBounds', true);
clearBoxShadow(nativeView);
}
if (nativeView.hasNonUniformBorder) {
unsubscribeFromScrollNotifications(view);
clearNonUniformBorders(nativeView);
}
iosViewUtils.clearGradient(nativeView);
if (background.image instanceof LinearGradient) {
iosViewUtils.drawGradient(nativeView, background.image);
}
const hasNonUniformBorderWidths = background.hasBorderWidth() && !background.hasUniformBorder();
const hasNonUniformBorderRadiuses = background.hasBorderRadius() && !background.hasUniformBorderRadius();
if (background.hasUniformBorderColor() && (hasNonUniformBorderWidths || hasNonUniformBorderRadiuses)) {
drawUniformColorNonUniformBorders(nativeView, background);
subscribeForScrollNotifications(view);
} else if (background.hasUniformBorder()) {
const layer = nativeView.layer;
const borderColor = background.getUniformBorderColor();
layer.borderColor = !borderColor ? undefined : borderColor.ios.CGColor;
layer.borderWidth = layout.toDeviceIndependentPixels(background.getUniformBorderWidth());
const renderSize = view.getActualSize() || { width: 0, height: 0 };
const cornerRadius = layout.toDeviceIndependentPixels(background.getUniformBorderRadius());
layer.cornerRadius = Math.min(Math.min(renderSize.width / 2, renderSize.height / 2), cornerRadius);
} else {
drawNoRadiusNonUniformBorders(nativeView, background);
subscribeForScrollNotifications(view);
}
// Clip-path should be called after borders are applied.
// It will eventually move them to different layer if uniform.
if (background.clipPath) {
drawClipPath(nativeView, background);
}
if (!background.image || background.image instanceof LinearGradient) {
const uiColor = background.color ? background.color.ios : undefined;
callback(uiColor);
} else {
setUIColorFromImage(view, nativeView, callback, flip);
}
if (background.hasBoxShadow()) {
drawBoxShadow(nativeView, view, background.getBoxShadow(), background);
}
// reset clear flags
background.clearFlags = BackgroundClearFlags.NONE;
}
}
function onScroll(this: void, args: ScrollEventData): void {
const view = <View>args.object;
const nativeView = view.nativeViewProtected;
if (nativeView instanceof UIScrollView) {
adjustLayersForScrollView(<any>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.
CATransaction.begin();
CATransaction.setValueForKey(kCFBooleanTrue, kCATransactionDisableActions);
const offset = nativeView.contentOffset;
const transform = {
a: 1,
b: 0,
c: 0,
d: 1,
tx: offset.x,
ty: offset.y,
};
layer.setAffineTransform(transform);
if (nativeView.layer.mask) {
nativeView.layer.mask.setAffineTransform(transform);
}
CATransaction.commit();
}
}
function unsubscribeFromScrollNotifications(view: View) {
if (view.nativeViewProtected instanceof UIScrollView) {
view.off('scroll', onScroll);
}
}
function subscribeForScrollNotifications(view: View) {
if (view.nativeViewProtected instanceof UIScrollView) {
view.on('scroll', onScroll);
adjustLayersForScrollView(<any>view.nativeViewProtected);
}
}
function clearNonUniformBorders(nativeView: NativeScriptUIView): void {
if (nativeView.borderLayer) {
nativeView.borderLayer.removeFromSuperlayer();
}
if (nativeView.hasBorderMask) {
nativeView.layer.mask = nativeView.borderOriginalMask;
nativeView.hasBorderMask = false;
nativeView.borderOriginalMask = null;
}
if (nativeView.topBorderLayer) {
nativeView.topBorderLayer.removeFromSuperlayer();
}
if (nativeView.rightBorderLayer) {
nativeView.rightBorderLayer.removeFromSuperlayer();
}
if (nativeView.bottomBorderLayer) {
nativeView.bottomBorderLayer.removeFromSuperlayer();
}
if (nativeView.leftBorderLayer) {
nativeView.leftBorderLayer.removeFromSuperlayer();
}
}
const pattern = /url\(('|")(.*?)\1\)/;
function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const frame = nativeView.frame;
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height;
if (!boundsWidth || !boundsHeight) {
return undefined;
}
const style = view.style;
const background = style.backgroundInternal;
let imageUri = background.image as string;
if (imageUri) {
const match = imageUri.match(pattern);
if (match && match[2]) {
imageUri = match[2];
}
}
let bitmap: UIImage;
if (isDataURI(imageUri)) {
const base64Data = imageUri.split(',')[1];
if (base64Data !== undefined) {
const imageSource = ImageSource.fromBase64Sync(base64Data);
bitmap = imageSource && imageSource.ios;
}
} else if (isFileOrResourcePath(imageUri)) {
const imageSource = ImageSource.fromFileOrResourceSync(imageUri);
bitmap = imageSource && imageSource.ios;
} else if (imageUri.indexOf('http') !== -1) {
style[symbolUrl] = imageUri;
ImageSource.fromUrl(imageUri)
.then((r) => {
if (style && style[symbolUrl] === imageUri) {
uiColorFromImage(r.ios, view, callback, flip);
}
})
.catch(() => {});
}
uiColorFromImage(bitmap, view, callback, flip);
}
interface BackgroundDrawParams {
repeatX: boolean;
repeatY: boolean;
posX: number;
posY: number;
sizeX?: number;
sizeY?: number;
}
function parsePosition(pos: string): { x: CSSValue; y: CSSValue } {
const values = cssParse(pos);
if (values.length === 2) {
return { x: values[0], y: values[1] };
}
if (values.length === 1) {
const center = { type: 'ident', string: 'center' };
if (values[0].type === 'ident') {
const val = values[0].string.toLocaleLowerCase();
// If you only one keyword is specified, the other value is "center"
if (val === 'left' || val === 'right') {
return { x: values[0], y: center };
} else if (val === 'top' || val === 'bottom') {
return { x: center, y: values[0] };
} else if (val === 'center') {
return { x: center, y: center };
}
} else if (values[0].type === 'number') {
return { x: values[0], y: center };
}
}
return null;
}
function getDrawParams(this: void, image: UIImage, background: BackgroundDefinition, width: number, height: number): BackgroundDrawParams {
if (!image) {
return null;
}
const res: BackgroundDrawParams = {
repeatX: true,
repeatY: true,
posX: 0,
posY: 0,
};
// repeat
if (background.repeat) {
switch (background.repeat.toLowerCase()) {
case 'no-repeat':
res.repeatX = false;
res.repeatY = false;
break;
case 'repeat-x':
res.repeatY = false;
break;
case 'repeat-y':
res.repeatX = false;
break;
}
}
const imageSize = image.size;
let imageWidth = imageSize.width;
let imageHeight = imageSize.height;
// size
const size = background.size;
if (size) {
const values = cssParse(size);
if (values.length === 2) {
const vx = values[0];
const vy = values[1];
if (vx.unit === '%' && vy.unit === '%') {
imageWidth = (width * vx.value) / 100;
imageHeight = (height * vy.value) / 100;
res.sizeX = imageWidth;
res.sizeY = imageHeight;
} else if (vx.type === 'number' && vy.type === 'number' && ((vx.unit === 'px' && vy.unit === 'px') || (vx.unit === '' && vy.unit === ''))) {
imageWidth = vx.value;
imageHeight = vy.value;
res.sizeX = imageWidth;
res.sizeY = imageHeight;
}
} else if (values.length === 1 && values[0].type === 'ident') {
let scale = 0;
if (values[0].string === 'cover') {
scale = Math.max(width / imageWidth, height / imageHeight);
} else if (values[0].string === 'contain') {
scale = Math.min(width / imageWidth, height / imageHeight);
}
if (scale > 0) {
imageWidth *= scale;
imageHeight *= scale;
res.sizeX = imageWidth;
res.sizeY = imageHeight;
}
}
}
// position
const position = background.position;
if (position) {
const v = parsePosition(position);
if (v) {
const spaceX = width - imageWidth;
const spaceY = height - imageHeight;
if (v.x.unit === '%' && v.y.unit === '%') {
res.posX = (spaceX * v.x.value) / 100;
res.posY = (spaceY * v.y.value) / 100;
} else if (v.x.type === 'number' && v.y.type === 'number' && ((v.x.unit === 'px' && v.y.unit === 'px') || (v.x.unit === '' && v.y.unit === ''))) {
res.posX = v.x.value;
res.posY = v.y.value;
} else if (v.x.type === 'ident' && v.y.type === 'ident') {
if (v.x.string.toLowerCase() === 'center') {
res.posX = spaceX / 2;
} else if (v.x.string.toLowerCase() === 'right') {
res.posX = spaceX;
}
if (v.y.string.toLowerCase() === 'center') {
res.posY = spaceY / 2;
} else if (v.y.string.toLowerCase() === 'bottom') {
res.posY = spaceY;
}
} else if (v.x.type === 'number' && v.y.type === 'ident') {
if (v.x.unit === '%') {
res.posX = (spaceX * v.x.value) / 100;
} else if (v.x.unit === 'px' || v.x.unit === '') {
res.posX = v.x.value;
}
if (v.y.string.toLowerCase() === 'center') {
res.posY = spaceY / 2;
} else if (v.y.string.toLowerCase() === 'bottom') {
res.posY = spaceY;
}
}
}
}
return res;
}
function uiColorFromImage(img: UIImage, view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
const background = view.style.backgroundInternal;
if (!img || !view.nativeViewProtected) {
callback(background.color && background.color.ios);
return;
}
const nativeView = view.nativeViewProtected as UIView;
const frame = nativeView.frame;
const boundsWidth = view.scaleX ? frame.size.width / view.scaleX : frame.size.width;
const boundsHeight = view.scaleY ? frame.size.height / view.scaleY : frame.size.height;
const params = getDrawParams(img, background, boundsWidth, boundsHeight);
if (params.sizeX > 0 && params.sizeY > 0) {
const resizeRect = CGRectMake(0, 0, params.sizeX, params.sizeY);
UIGraphicsBeginImageContextWithOptions(resizeRect.size, false, 0.0);
img.drawInRect(resizeRect);
img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
UIGraphicsBeginImageContextWithOptions(CGSizeFromString(`{${boundsWidth},${boundsHeight}}`), false, 0.0);
const context = UIGraphicsGetCurrentContext();
if (background.color && background.color.ios) {
CGContextSetFillColorWithColor(context, background.color.ios.CGColor);
CGContextFillRect(context, CGRectMake(0, 0, boundsWidth, boundsHeight));
}
if (!params.repeatX && !params.repeatY) {
img.drawAtPoint(CGPointMake(params.posX, params.posY));
} else {
const w = params.repeatX ? boundsWidth : img.size.width;
const h = params.repeatY ? boundsHeight : img.size.height;
CGContextSetPatternPhase(context, CGSizeMake(params.posX, params.posY));
params.posX = params.repeatX ? 0 : params.posX;
params.posY = params.repeatY ? 0 : params.posY;
const patternRect = CGRectMake(params.posX, params.posY, w, h);
img.drawAsPatternInRect(patternRect);
}
const bkgImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (flip) {
const flippedImage = _flipImage(bkgImage);
callback(UIColor.alloc().initWithPatternImage(flippedImage));
} else {
callback(UIColor.alloc().initWithPatternImage(bkgImage));
}
}
// Flipping the default coordinate system
// https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html
function _flipImage(originalImage: UIImage): UIImage {
UIGraphicsBeginImageContextWithOptions(originalImage.size, false, 0.0);
const context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0.0, originalImage.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
originalImage.drawInRect(CGRectMake(0, 0, originalImage.size.width, originalImage.size.height));
CGContextRestoreGState(context);
const flippedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return flippedImage;
}
function cssValueToDeviceIndependentPixels(source: string, total: number): number {
source = source.trim();
if (source.indexOf('px') !== -1) {
return layout.toDeviceIndependentPixels(parseFloat(source.replace('px', '')));
} else if (source.indexOf('%') !== -1 && total > 0) {
return (parseFloat(source.replace('%', '')) / 100) * total;
} else {
return parseFloat(source);
}
}
function drawUniformColorNonUniformBorders(nativeView: NativeScriptUIView, background: BackgroundDefinition) {
const layer = nativeView.layer;
layer.backgroundColor = undefined;
layer.borderColor = undefined;
layer.borderWidth = 0;
layer.cornerRadius = 0;
const { width, height } = layer.bounds.size;
const { x, y } = layer.bounds.origin;
const left = x;
const top = y;
const right = x + width;
const bottom = y + height;
const { min, max } = Math;
const borderTopWidth = max(0, layout.toDeviceIndependentPixels(background.borderTopWidth));
const borderRightWidth = max(0, layout.toDeviceIndependentPixels(background.borderRightWidth));
const borderBottomWidth = max(0, layout.toDeviceIndependentPixels(background.borderBottomWidth));
const borderLeftWidth = max(0, layout.toDeviceIndependentPixels(background.borderLeftWidth));
const borderVWidth = borderTopWidth + borderBottomWidth;
const borderHWidth = borderLeftWidth + borderRightWidth;
const cappedBorderTopWidth = borderTopWidth && borderTopWidth * min(1, height / borderVWidth);
const cappedBorderRightWidth = borderRightWidth && borderRightWidth * min(1, width / borderHWidth);
const cappedBorderBottomWidth = borderBottomWidth && borderBottomWidth * min(1, height / borderVWidth);
const cappedBorderLeftWidth = borderLeftWidth && borderLeftWidth * min(1, width / borderHWidth);
const outerTopLeftRadius = layout.toDeviceIndependentPixels(background.borderTopLeftRadius);
const outerTopRightRadius = layout.toDeviceIndependentPixels(background.borderTopRightRadius);
const outerBottomRightRadius = layout.toDeviceIndependentPixels(background.borderBottomRightRadius);
const outerBottomLeftRadius = layout.toDeviceIndependentPixels(background.borderBottomLeftRadius);
const topRadii = outerTopLeftRadius + outerTopRightRadius;
const rightRadii = outerTopRightRadius + outerBottomRightRadius;
const bottomRadii = outerBottomRightRadius + outerBottomLeftRadius;
const leftRadii = outerBottomLeftRadius + outerTopLeftRadius;
function capRadius(a: number, b: number, c: number): number {
return a && Math.min(a, Math.min(b, c));
}
const cappedOuterTopLeftRadius = capRadius(outerTopLeftRadius, (outerTopLeftRadius / topRadii) * width, (outerTopLeftRadius / leftRadii) * height);
const cappedOuterTopRightRadius = capRadius(outerTopRightRadius, (outerTopRightRadius / topRadii) * width, (outerTopRightRadius / rightRadii) * height);
const cappedOuterBottomRightRadius = capRadius(outerBottomRightRadius, (outerBottomRightRadius / bottomRadii) * width, (outerBottomRightRadius / rightRadii) * height);
const cappedOuterBottomLeftRadius = capRadius(outerBottomLeftRadius, (outerBottomLeftRadius / bottomRadii) * width, (outerBottomLeftRadius / leftRadii) * height);
// Outer contour
const clipPath = CGPathCreateMutable();
CGPathMoveToPoint(clipPath, null, left + cappedOuterTopLeftRadius, top);
CGPathAddArcToPoint(clipPath, null, right, top, right, top + cappedOuterTopRightRadius, cappedOuterTopRightRadius);
CGPathAddArcToPoint(clipPath, null, right, bottom, right - cappedOuterBottomRightRadius, bottom, cappedOuterBottomRightRadius);
CGPathAddArcToPoint(clipPath, null, left, bottom, left, bottom - cappedOuterBottomLeftRadius, cappedOuterBottomLeftRadius);
CGPathAddArcToPoint(clipPath, null, left, top, left + cappedOuterTopLeftRadius, top, cappedOuterTopLeftRadius);
CGPathCloseSubpath(clipPath);
nativeView.borderOriginalMask = layer.mask;
const clipShapeLayer = CAShapeLayer.layer();
clipShapeLayer.path = clipPath;
layer.mask = clipShapeLayer;
nativeView.hasBorderMask = true;
if (cappedBorderLeftWidth > 0 || cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0 || cappedBorderBottomWidth > 0) {
const borderPath = CGPathCreateMutable();
CGPathAddRect(borderPath, null, CGRectMake(left, top, width, height));
// Inner contour
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
CGPathMoveToPoint(borderPath, null, left + cappedOuterTopLeftRadius, top + cappedBorderTopWidth);
} else {
CGPathMoveToPoint(borderPath, null, left, top);
}
if (cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0) {
const innerTopRightWRadius = max(0, cappedOuterTopRightRadius - cappedBorderRightWidth);
const innerTopRightHRadius = max(0, cappedOuterTopRightRadius - cappedBorderTopWidth);
const innerTopRightMaxRadius = max(innerTopRightWRadius, innerTopRightHRadius);
const innerTopRightTransform: any = CGAffineTransformMake(innerTopRightMaxRadius && innerTopRightWRadius / innerTopRightMaxRadius, 0, 0, innerTopRightMaxRadius && innerTopRightHRadius / innerTopRightMaxRadius, right - cappedBorderRightWidth - innerTopRightWRadius, top + cappedBorderTopWidth + innerTopRightHRadius);
CGPathAddArc(borderPath, innerTopRightTransform, 0, 0, innerTopRightMaxRadius, (Math.PI * 3) / 2, 0, false);
} else {
CGPathMoveToPoint(borderPath, null, right, top);
}
if (cappedBorderBottomWidth > 0 || cappedBorderRightWidth > 0) {
const innerBottomRightWRadius = max(0, cappedOuterBottomRightRadius - cappedBorderRightWidth);
const innerBottomRightHRadius = max(0, cappedOuterBottomRightRadius - cappedBorderBottomWidth);
const innerBottomRightMaxRadius = max(innerBottomRightWRadius, innerBottomRightHRadius);
const innerBottomRightTransform: any = CGAffineTransformMake(innerBottomRightMaxRadius && innerBottomRightWRadius / innerBottomRightMaxRadius, 0, 0, innerBottomRightMaxRadius && innerBottomRightHRadius / innerBottomRightMaxRadius, right - cappedBorderRightWidth - innerBottomRightWRadius, bottom - cappedBorderBottomWidth - innerBottomRightHRadius);
CGPathAddArc(borderPath, innerBottomRightTransform, 0, 0, innerBottomRightMaxRadius, 0, Math.PI / 2, false);
} else {
CGPathAddLineToPoint(borderPath, null, right, bottom);
}
if (cappedBorderBottomWidth > 0 || cappedBorderLeftWidth > 0) {
const innerBottomLeftWRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderLeftWidth);
const innerBottomLeftHRadius = max(0, cappedOuterBottomLeftRadius - cappedBorderBottomWidth);
const innerBottomLeftMaxRadius = max(innerBottomLeftWRadius, innerBottomLeftHRadius);
const innerBottomLeftTransform: any = CGAffineTransformMake(innerBottomLeftMaxRadius && innerBottomLeftWRadius / innerBottomLeftMaxRadius, 0, 0, innerBottomLeftMaxRadius && innerBottomLeftHRadius / innerBottomLeftMaxRadius, left + cappedBorderLeftWidth + innerBottomLeftWRadius, bottom - cappedBorderBottomWidth - innerBottomLeftHRadius);
CGPathAddArc(borderPath, innerBottomLeftTransform, 0, 0, innerBottomLeftMaxRadius, Math.PI / 2, Math.PI, false);
} else {
CGPathAddLineToPoint(borderPath, null, left, bottom);
}
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
const innerTopLeftWRadius = max(0, cappedOuterTopLeftRadius - cappedBorderLeftWidth);
const innerTopLeftHRadius = max(0, cappedOuterTopLeftRadius - cappedBorderTopWidth);
const innerTopLeftMaxRadius = max(innerTopLeftWRadius, innerTopLeftHRadius);
const innerTopLeftTransform: any = CGAffineTransformMake(innerTopLeftMaxRadius && innerTopLeftWRadius / innerTopLeftMaxRadius, 0, 0, innerTopLeftMaxRadius && innerTopLeftHRadius / innerTopLeftMaxRadius, left + cappedBorderLeftWidth + innerTopLeftWRadius, top + cappedBorderTopWidth + innerTopLeftHRadius);
CGPathAddArc(borderPath, innerTopLeftTransform, 0, 0, innerTopLeftMaxRadius, Math.PI, (Math.PI * 3) / 2, false);
} else {
CGPathAddLineToPoint(borderPath, null, left, top);
}
CGPathCloseSubpath(borderPath);
const borderLayer = CAShapeLayer.layer();
borderLayer.fillColor = (background.borderTopColor && background.borderTopColor.ios.CGColor) || UIColor.blackColor.CGColor;
borderLayer.fillRule = kCAFillRuleEvenOdd;
borderLayer.path = borderPath;
layer.addSublayer(borderLayer);
nativeView.borderLayer = borderLayer;
}
nativeView.hasNonUniformBorder = true;
}
function drawNoRadiusNonUniformBorders(nativeView: NativeScriptUIView, background: BackgroundDefinition) {
const borderLayer = CALayer.layer();
nativeView.layer.addSublayer(borderLayer);
nativeView.borderLayer = borderLayer;
borderLayer.borderColor = undefined;
borderLayer.borderWidth = 0;
borderLayer.cornerRadius = 0;
const layerBounds = nativeView.layer.bounds;
const layerOrigin = layerBounds.origin;
const layerSize = layerBounds.size;
const nativeViewLayerBounds = {
left: layerOrigin.x,
top: layerOrigin.y,
bottom: layerSize.height,
right: layerSize.width,
};
const top = layout.toDeviceIndependentPixels(background.borderTopWidth);
const right = layout.toDeviceIndependentPixels(background.borderRightWidth);
const bottom = layout.toDeviceIndependentPixels(background.borderBottomWidth);
const left = layout.toDeviceIndependentPixels(background.borderLeftWidth);
const lto: Point = {
x: nativeViewLayerBounds.left,
y: nativeViewLayerBounds.top,
}; // left-top-outside
const lti: Point = {
x: nativeViewLayerBounds.left + left,
y: nativeViewLayerBounds.top + top,
}; // left-top-inside
const rto: Point = {
x: nativeViewLayerBounds.right,
y: nativeViewLayerBounds.top,
}; // right-top-outside
const rti: Point = {
x: nativeViewLayerBounds.right - right,
y: nativeViewLayerBounds.top + top,
}; // right-top-inside
const rbo: Point = {
x: nativeViewLayerBounds.right,
y: nativeViewLayerBounds.bottom,
}; // right-bottom-outside
const rbi: Point = {
x: nativeViewLayerBounds.right - right,
y: nativeViewLayerBounds.bottom - bottom,
}; // right-bottom-inside
const lbo: Point = {
x: nativeViewLayerBounds.left,
y: nativeViewLayerBounds.bottom,
}; // left-bottom-outside
const lbi: Point = {
x: nativeViewLayerBounds.left + left,
y: nativeViewLayerBounds.bottom - bottom,
}; // left-bottom-inside
let hasNonUniformBorder: boolean;
const borderTopColor = background.borderTopColor;
if (top > 0 && borderTopColor && borderTopColor.ios) {
const topBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(topBorderPath, null, lto.x, lto.y);
CGPathAddLineToPoint(topBorderPath, null, rto.x, rto.y);
CGPathAddLineToPoint(topBorderPath, null, rti.x, rti.y);
CGPathAddLineToPoint(topBorderPath, null, lti.x, lti.y);
CGPathAddLineToPoint(topBorderPath, null, lto.x, lto.y);
const topBorderLayer = CAShapeLayer.layer();
topBorderLayer.fillColor = background.borderTopColor.ios.CGColor;
topBorderLayer.path = topBorderPath;
borderLayer.addSublayer(topBorderLayer);
nativeView.topBorderLayer = topBorderLayer;
hasNonUniformBorder = true;
}
const borderRightColor = background.borderRightColor;
if (right > 0 && borderRightColor && borderRightColor.ios) {
const rightBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(rightBorderPath, null, rto.x, rto.y);
CGPathAddLineToPoint(rightBorderPath, null, rbo.x, rbo.y);
CGPathAddLineToPoint(rightBorderPath, null, rbi.x, rbi.y);
CGPathAddLineToPoint(rightBorderPath, null, rti.x, rti.y);
CGPathAddLineToPoint(rightBorderPath, null, rto.x, rto.y);
const rightBorderLayer = CAShapeLayer.layer();
rightBorderLayer.fillColor = background.borderRightColor.ios.CGColor;
rightBorderLayer.path = rightBorderPath;
borderLayer.addSublayer(rightBorderLayer);
nativeView.rightBorderLayer = rightBorderLayer;
hasNonUniformBorder = true;
}
const borderBottomColor = background.borderBottomColor;
if (bottom > 0 && borderBottomColor && borderBottomColor.ios) {
const bottomBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(bottomBorderPath, null, rbo.x, rbo.y);
CGPathAddLineToPoint(bottomBorderPath, null, lbo.x, lbo.y);
CGPathAddLineToPoint(bottomBorderPath, null, lbi.x, lbi.y);
CGPathAddLineToPoint(bottomBorderPath, null, rbi.x, rbi.y);
CGPathAddLineToPoint(bottomBorderPath, null, rbo.x, rbo.y);
const bottomBorderLayer = CAShapeLayer.layer();
bottomBorderLayer.fillColor = background.borderBottomColor.ios.CGColor;
bottomBorderLayer.path = bottomBorderPath;
borderLayer.addSublayer(bottomBorderLayer);
nativeView.bottomBorderLayer = bottomBorderLayer;
hasNonUniformBorder = true;
}
const borderLeftColor = background.borderLeftColor;
if (left > 0 && borderLeftColor && borderLeftColor.ios) {
const leftBorderPath = CGPathCreateMutable();
CGPathMoveToPoint(leftBorderPath, null, lbo.x, lbo.y);
CGPathAddLineToPoint(leftBorderPath, null, lto.x, lto.y);
CGPathAddLineToPoint(leftBorderPath, null, lti.x, lti.y);
CGPathAddLineToPoint(leftBorderPath, null, lbi.x, lbi.y);
CGPathAddLineToPoint(leftBorderPath, null, lbo.x, lbo.y);
const leftBorderLayer = CAShapeLayer.layer();
leftBorderLayer.fillColor = background.borderLeftColor.ios.CGColor;
leftBorderLayer.path = leftBorderPath;
borderLayer.addSublayer(leftBorderLayer);
nativeView.leftBorderLayer = leftBorderLayer;
hasNonUniformBorder = true;
}
nativeView.hasNonUniformBorder = hasNonUniformBorder;
}
// TODO: use sublayer if its applied to a layout
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;
nativeView.clipsToBounds = false;
// this is required (if not, shadow will get cutoff at parent's dimensions)
// nativeView.clipsToBounds doesn't work
view.setProperty('clipToBounds', false);
if (!background.color?.a) {
// add white background if view has a transparent background
layer.backgroundColor = UIColor.whiteColor.CGColor;
}
// shadow opacity is handled on the shadow's color instance
layer.shadowOpacity = boxShadow.color?.a ? boxShadow.color?.a / 255 : 1;
layer.shadowRadius = Length.toDevicePixels(boxShadow.blurRadius, 0.0);
layer.shadowColor = boxShadow.color.ios.CGColor;
// prettier-ignore
layer.shadowOffset = CGSizeMake(
Length.toDevicePixels(boxShadow.offsetX, 0.0),
Length.toDevicePixels(boxShadow.offsetY, 0.0)
);
// this should match the view's border radius
let cornerRadius: number;
if (typeof view.style.borderRadius !== 'number') {
cornerRadius = Length.toDevicePixels(<CoreTypes.LengthType>view.style.borderRadius, 0.0);
} else {
cornerRadius = view.style.borderRadius;
}
// apply spreadRadius by expanding shadow layer bounds
// prettier-ignore
const bounds = CGRectInset(nativeView.bounds,
-Length.toDevicePixels(boxShadow.spreadRadius, 0.0),
-Length.toDevicePixels(boxShadow.spreadRadius, 0.0)
);
// This has the nice glow with box shadow of 0,0
layer.shadowPath = UIBezierPath.bezierPathWithRoundedRectCornerRadius(bounds, cornerRadius).CGPath;
}
function clearBoxShadow(nativeView: NativeScriptUIView) {
nativeView.clipsToBounds = true;
const layer: CALayer = iOSNativeHelper.getShadowLayer(nativeView, 'ns-box-shadow', false);
if (!layer) {
return;
}
layer.masksToBounds = true;
layer.shadowOffset = CGSizeMake(0, 0);
layer.shadowColor = UIColor.clearColor.CGColor;
layer.cornerRadius = 0.0;
layer.shadowRadius = 0.0;
layer.shadowOpacity = 0.0;
}
function drawClipPath(nativeView: UIView, background: BackgroundDefinition) {
const layer = nativeView.layer;
const layerBounds = layer.bounds;
const layerOrigin = layerBounds.origin;
const layerSize = layerBounds.size;
const bounds = {
left: layerOrigin.x,
top: layerOrigin.y,
bottom: layerSize.height,
right: layerSize.width,
};
if (bounds.right === 0 || bounds.bottom === 0) {
return;
}
let path: UIBezierPath;
const clipPath = background.clipPath;
const functionName = clipPath.substring(0, clipPath.indexOf('('));
const value = clipPath.replace(`${functionName}(`, '').replace(')', '');
switch (functionName) {
case 'rect':
path = rectPath(value, bounds);
break;
case 'inset':
path = insetPath(value, bounds);
break;
case 'circle':
path = circlePath(value, bounds);
break;
case 'ellipse':
path = ellipsePath(value, bounds);
break;
case 'polygon':
path = polygonPath(value, bounds);
break;
}
if (path) {
const shape = CAShapeLayer.layer();
shape.path = path;
layer.mask = shape;
nativeView.clipsToBounds = true;
const borderWidth = background.getUniformBorderWidth();
const borderColor = background.getUniformBorderColor();
if (borderWidth > 0 && borderColor instanceof Color) {
const borderLayer = CAShapeLayer.layer();
borderLayer.path = path;
borderLayer.lineWidth = borderWidth * 2;
borderLayer.strokeColor = borderColor.ios.CGColor;
borderLayer.fillColor = clearCGColor;
borderLayer.frame = nativeView.bounds;
layer.borderColor = undefined;
layer.borderWidth = 0;
layer.addSublayer(borderLayer);
}
}
}
function rectPath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
const top = cssValueToDeviceIndependentPixels(arr[0], bounds.top);
const right = cssValueToDeviceIndependentPixels(arr[1], bounds.right);
const bottom = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
const left = cssValueToDeviceIndependentPixels(arr[3], bounds.left);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
function insetPath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
let topString: string;
let rightString: string;
let bottomString: string;
let leftString: string;
if (arr.length === 1) {
topString = rightString = bottomString = leftString = arr[0];
} else if (arr.length === 2) {
topString = bottomString = arr[0];
rightString = leftString = arr[1];
} else if (arr.length === 3) {
topString = arr[0];
rightString = leftString = arr[1];
bottomString = arr[2];
} else if (arr.length === 4) {
topString = arr[0];
rightString = arr[1];
bottomString = arr[2];
leftString = arr[3];
}
const top = cssValueToDeviceIndependentPixels(topString, bounds.bottom);
const right = cssValueToDeviceIndependentPixels('100%', bounds.right) - cssValueToDeviceIndependentPixels(rightString, bounds.right);
const bottom = cssValueToDeviceIndependentPixels('100%', bounds.bottom) - cssValueToDeviceIndependentPixels(bottomString, bounds.bottom);
const left = cssValueToDeviceIndependentPixels(leftString, bounds.right);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
function circlePath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
const radius = cssValueToDeviceIndependentPixels(arr[0], (bounds.right > bounds.bottom ? bounds.bottom : bounds.right) / 2);
const y = cssValueToDeviceIndependentPixels(arr[2], bounds.bottom);
const x = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
return UIBezierPath.bezierPathWithArcCenterRadiusStartAngleEndAngleClockwise(CGPointMake(x, y), radius, 0, 360, true).CGPath;
}
function ellipsePath(value: string, bounds: Rect): UIBezierPath {
const arr = value.split(/[\s]+/);
const rX = cssValueToDeviceIndependentPixels(arr[0], bounds.right);
const rY = cssValueToDeviceIndependentPixels(arr[1], bounds.bottom);
const cX = cssValueToDeviceIndependentPixels(arr[3], bounds.right);
const cY = cssValueToDeviceIndependentPixels(arr[4], bounds.bottom);
const left = cX - rX;
const top = cY - rY;
const width = rX * 2;
const height = rY * 2;
return UIBezierPath.bezierPathWithOvalInRect(CGRectMake(left, top, width, height)).CGPath;
}
function polygonPath(value: string, bounds: Rect): UIBezierPath {
const path = CGPathCreateMutable();
let firstPoint: Point;
const arr = value.split(/[,]+/);
for (let i = 0; i < arr.length; i++) {
const xy = arr[i].trim().split(/[\s]+/);
const point: Point = {
x: cssValueToDeviceIndependentPixels(xy[0], bounds.right),
y: cssValueToDeviceIndependentPixels(xy[1], bounds.bottom),
};
if (!firstPoint) {
firstPoint = point;
CGPathMoveToPoint(path, null, point.x, point.y);
}
CGPathAddLineToPoint(path, null, point.x, point.y);
}
CGPathAddLineToPoint(path, null, firstPoint.x, firstPoint.y);
return path;
}