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 { 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 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; 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 = view.nativeViewProtected; 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); } clearGradient(nativeView); if (background.image instanceof LinearGradient) { 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 = args.object; const nativeView = view.nativeViewProtected; if (nativeView instanceof UIScrollView) { adjustLayersForScrollView(nativeView); } } function adjustLayersForScrollView(nativeView: UIScrollView & NativeView) { 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(view.nativeViewProtected); } } function clearNonUniformBorders(nativeView: NativeView): 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); } }); } 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) { 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: NativeView, 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: NativeView, 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: NativeView, 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 const cornerRadius = Length.toDevicePixels(view.style.borderRadius, 0.0); // 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: NativeView) { 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 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; 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; }