@nativescript/core
Version:
A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.
1,060 lines • 51.6 kB
JavaScript
import { LinearGradient } from './linear-gradient';
import { Screen } from '../../platform';
import { isDataURI, isFileOrResourcePath, layout } from '../../utils';
import { ios as iosViewUtils } from '../utils';
import { ImageSource } from '../../image-source';
import { parse as cssParse } from '../../css-value/reworkcss-value.js';
import { ClipPathFunction } from './clip-path-function';
export * from './background-common';
const clearCGColor = UIColor.clearColor.CGColor;
const uriPattern = /url\(('|")(.*?)\1\)/;
const symbolUrl = Symbol('backgroundImageUrl');
export var CacheMode;
(function (CacheMode) {
CacheMode[CacheMode["none"] = 0] = "none";
})(CacheMode || (CacheMode = {}));
export var ios;
(function (ios) {
function createBackgroundUIColor(view, callback, flip) {
const background = view.style.backgroundInternal;
const nativeView = view.nativeViewProtected;
if (!nativeView) {
return;
}
// Unset this in case another layer handles background color (e.g. gradient)
nativeView.layer.backgroundColor = null;
// Cleanup of previous values
clearBackgroundVisualEffects(view);
// Borders, shadows, etc
drawBackgroundVisualEffects(view);
if (!background.image) {
callback(background?.color?.ios);
}
else {
if (!(background.image instanceof LinearGradient)) {
createUIImageFromURI(view, background.image, flip, (image) => {
callback(image ? UIColor.alloc().initWithPatternImage(image) : background?.color?.ios);
});
}
}
}
ios.createBackgroundUIColor = createBackgroundUIColor;
function drawBackgroundVisualEffects(view) {
const background = view.style.backgroundInternal;
const nativeView = view.nativeViewProtected;
const layer = nativeView.layer;
let needsLayerAdjustmentOnScroll = false;
// Add new gradient layer or update existing one
if (background.image instanceof LinearGradient) {
if (!nativeView.gradientLayer) {
nativeView.gradientLayer = CAGradientLayer.new();
layer.insertSublayerAtIndex(nativeView.gradientLayer, 0);
}
iosViewUtils.drawGradient(nativeView, nativeView.gradientLayer, background.image);
needsLayerAdjustmentOnScroll = true;
}
// Initialize clipping mask (usually for clip-path and non-uniform rounded borders)
maskLayerIfNeeded(nativeView, background);
if (background.hasUniformBorder()) {
const borderColor = background.getUniformBorderColor();
layer.borderColor = borderColor?.ios?.CGColor;
layer.borderWidth = layout.toDeviceIndependentPixels(background.getUniformBorderWidth());
layer.cornerRadius = getUniformBorderRadius(view, layer.bounds);
}
else {
drawNonUniformBorders(nativeView, background);
needsLayerAdjustmentOnScroll = true;
}
// Clip-path should be called after borders are applied
if (nativeView.maskType === iosViewUtils.LayerMask.CLIP_PATH && layer.mask instanceof CAShapeLayer) {
layer.mask.path = generateClipPath(view, layer.bounds);
}
if (background.hasBoxShadow()) {
drawBoxShadow(view);
needsLayerAdjustmentOnScroll = true;
}
if (needsLayerAdjustmentOnScroll) {
registerAdjustLayersOnScrollListener(view);
}
}
ios.drawBackgroundVisualEffects = drawBackgroundVisualEffects;
function clearBackgroundVisualEffects(view) {
const nativeView = view.nativeViewProtected;
if (!nativeView) {
return;
}
const background = view.style.backgroundInternal;
const hasGradientBackground = background.image && background.image instanceof LinearGradient;
// Remove mask if there is no clip path or non-uniform border with radius
let needsMask;
switch (nativeView.maskType) {
case iosViewUtils.LayerMask.BORDER:
needsMask = !background.hasUniformBorder() && background.hasBorderRadius();
break;
case iosViewUtils.LayerMask.CLIP_PATH:
needsMask = !!background.clipPath;
break;
default:
needsMask = false;
break;
}
if (!needsMask) {
clearLayerMask(nativeView);
}
// Clear box shadow if it's no longer needed
if (background.clearFlags & 2 /* BackgroundClearFlags.CLEAR_BOX_SHADOW */) {
clearBoxShadow(nativeView);
}
// Non-uniform borders cleanup
if (nativeView.hasNonUniformBorder) {
if (nativeView.hasNonUniformBorderColor && background.hasUniformBorderColor()) {
clearNonUniformColorBorders(nativeView);
}
if (background.hasUniformBorder()) {
clearNonUniformBorders(nativeView);
}
}
if (nativeView.gradientLayer && !hasGradientBackground) {
nativeView.gradientLayer.removeFromSuperlayer();
nativeView.gradientLayer = null;
}
// Force unset scroll listener
unregisterAdjustLayersOnScrollListener(view);
// Reset clear flags
background.clearFlags = 0 /* BackgroundClearFlags.NONE */;
}
ios.clearBackgroundVisualEffects = clearBackgroundVisualEffects;
function createUIImageFromURI(view, imageURI, flip, callback) {
const nativeView = view.nativeViewProtected;
if (!nativeView) {
return;
}
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;
if (imageURI) {
const match = imageURI.match(uriPattern);
if (match && match[2]) {
imageURI = match[2];
}
}
let bitmap;
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) {
callback(generatePatternImage(r.ios, view, flip));
}
})
.catch(() => { });
}
callback(generatePatternImage(bitmap, view, flip));
}
ios.createUIImageFromURI = createUIImageFromURI;
function generateShadowLayerPaths(view, bounds) {
const background = view.style.backgroundInternal;
const nativeView = view.nativeViewProtected;
const layer = nativeView.layer;
const boxShadow = background.getBoxShadow();
const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
const { width, height } = bounds.size;
let innerPath, shadowPath;
// Generate more detailed paths if view has border radius
if (background.hasBorderRadius()) {
if (background.hasUniformBorder()) {
const cornerRadius = layer.cornerRadius;
const cappedRadius = getBorderCapRadius(cornerRadius, width / 2, height / 2);
const cappedOuterRadii = {
topLeft: cappedRadius,
topRight: cappedRadius,
bottomLeft: cappedRadius,
bottomRight: cappedRadius,
};
const cappedOuterRadiiWithSpread = {
topLeft: cappedRadius + spreadRadius,
topRight: cappedRadius + spreadRadius,
bottomLeft: cappedRadius + spreadRadius,
bottomRight: cappedRadius + spreadRadius,
};
innerPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadii);
shadowPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadiiWithSpread, spreadRadius);
}
else {
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;
const cappedOuterRadii = {
topLeft: getBorderCapRadius(outerTopLeftRadius, (outerTopLeftRadius / topRadii) * width, (outerTopLeftRadius / leftRadii) * height),
topRight: getBorderCapRadius(outerTopRightRadius, (outerTopRightRadius / topRadii) * width, (outerTopRightRadius / rightRadii) * height),
bottomLeft: getBorderCapRadius(outerBottomLeftRadius, (outerBottomLeftRadius / bottomRadii) * width, (outerBottomLeftRadius / leftRadii) * height),
bottomRight: getBorderCapRadius(outerBottomRightRadius, (outerBottomRightRadius / bottomRadii) * width, (outerBottomRightRadius / rightRadii) * height),
};
// Add spread radius to corners that actually have radius as shadow has grown larger
// than view itself and needs to be rounded accordingly
const cappedOuterRadiiWithSpread = {
topLeft: cappedOuterRadii.topLeft > 0 ? cappedOuterRadii.topLeft + spreadRadius : cappedOuterRadii.topLeft,
topRight: cappedOuterRadii.topRight > 0 ? cappedOuterRadii.topRight + spreadRadius : cappedOuterRadii.topRight,
bottomLeft: cappedOuterRadii.bottomLeft > 0 ? cappedOuterRadii.bottomLeft + spreadRadius : cappedOuterRadii.bottomLeft,
bottomRight: cappedOuterRadii.bottomRight > 0 ? cappedOuterRadii.bottomRight + spreadRadius : cappedOuterRadii.bottomRight,
};
innerPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadii);
shadowPath = generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadiiWithSpread, spreadRadius);
}
}
else {
innerPath = CGPathCreateWithRect(bounds, null);
shadowPath = CGPathCreateWithRect(CGRectInset(bounds, -spreadRadius, -spreadRadius), null);
}
return {
maskPath: generateShadowMaskPath(bounds, boxShadow, innerPath),
shadowPath,
};
}
ios.generateShadowLayerPaths = generateShadowLayerPaths;
function generateClipPath(view, bounds) {
const background = view.style.backgroundInternal;
const { origin, size } = bounds;
const position = {
left: origin.x,
top: origin.y,
bottom: size.height,
right: size.width,
};
if (position.right === 0 || position.bottom === 0) {
return;
}
let path;
const clipPath = background.clipPath;
if (clipPath instanceof ClipPathFunction) {
switch (clipPath.shape) {
case 'rect':
path = rectPath(clipPath.rule, position);
break;
case 'inset':
path = insetPath(clipPath.rule, position);
break;
case 'circle':
path = circlePath(clipPath.rule, position);
break;
case 'ellipse':
path = ellipsePath(clipPath.rule, position);
break;
case 'polygon':
path = polygonPath(clipPath.rule, position);
break;
}
}
else {
path = null;
}
return path;
}
ios.generateClipPath = generateClipPath;
function getUniformBorderRadius(view, bounds) {
const background = view.style.backgroundInternal;
const { width, height } = bounds.size;
const cornerRadius = layout.toDeviceIndependentPixels(background.getUniformBorderRadius());
return Math.min(Math.min(width / 2, height / 2), cornerRadius);
}
ios.getUniformBorderRadius = getUniformBorderRadius;
function generateNonUniformBorderInnerClipRoundedPath(view, bounds) {
const background = view.style.backgroundInternal;
const cappedOuterRadii = calculateNonUniformBorderCappedRadii(bounds, background);
return generateNonUniformBorderInnerClipPath(bounds, background, cappedOuterRadii);
}
ios.generateNonUniformBorderInnerClipRoundedPath = generateNonUniformBorderInnerClipRoundedPath;
function generateNonUniformBorderOuterClipRoundedPath(view, bounds) {
const background = view.style.backgroundInternal;
const cappedOuterRadii = calculateNonUniformBorderCappedRadii(bounds, background);
return generateNonUniformBorderOuterClipPath(bounds, cappedOuterRadii);
}
ios.generateNonUniformBorderOuterClipRoundedPath = generateNonUniformBorderOuterClipRoundedPath;
function generateNonUniformMultiColorBorderRoundedPaths(view, bounds) {
const background = view.style.backgroundInternal;
return generateNonUniformMultiColorBorderPaths(bounds, background);
}
ios.generateNonUniformMultiColorBorderRoundedPaths = generateNonUniformMultiColorBorderRoundedPaths;
})(ios || (ios = {}));
function maskLayerIfNeeded(nativeView, background) {
const layer = nativeView.layer;
// Check if layer should be masked
if (!(layer.mask instanceof CAShapeLayer)) {
// Since layers can only accept up to a single mask at a time, clip path is given more priority
if (background.clipPath) {
nativeView.maskType = iosViewUtils.LayerMask.CLIP_PATH;
}
else if (!background.hasUniformBorder() && background.hasBorderRadius()) {
nativeView.maskType = iosViewUtils.LayerMask.BORDER;
}
else {
nativeView.maskType = null;
}
if (nativeView.maskType != null) {
nativeView.originalMask = layer.mask;
layer.mask = CAShapeLayer.new();
}
}
}
function clearLayerMask(nativeView) {
if (nativeView.outerShadowContainerLayer) {
nativeView.outerShadowContainerLayer.mask = null;
}
nativeView.layer.mask = nativeView.originalMask;
nativeView.originalMask = null;
nativeView.maskType = null;
}
function onBackgroundViewScroll(args) {
const view = args.object;
const nativeView = view.nativeViewProtected;
if (nativeView instanceof UIScrollView) {
adjustLayersForScrollView(nativeView);
}
}
function adjustLayersForScrollView(nativeView) {
// Compensates with transition for the background layers for scrolling in ScrollView based controls.
CATransaction.begin();
CATransaction.setDisableActions(true);
const offset = nativeView.contentOffset;
const transform = {
a: 1,
b: 0,
c: 0,
d: 1,
tx: offset.x,
ty: offset.y,
};
if (nativeView.layer.mask) {
nativeView.layer.mask.setAffineTransform(transform);
}
// Nested layers
if (nativeView.gradientLayer) {
nativeView.gradientLayer.setAffineTransform(transform);
}
if (nativeView.borderLayer) {
nativeView.borderLayer.setAffineTransform(transform);
}
if (nativeView.outerShadowContainerLayer) {
// Update bounds of shadow layer as it belongs to parent view
nativeView.outerShadowContainerLayer.bounds = nativeView.bounds;
nativeView.outerShadowContainerLayer.setAffineTransform(transform);
}
CATransaction.setDisableActions(false);
CATransaction.commit();
}
function unregisterAdjustLayersOnScrollListener(view) {
if (view.nativeViewProtected instanceof UIScrollView) {
view.off('scroll', onBackgroundViewScroll);
}
}
function registerAdjustLayersOnScrollListener(view) {
if (view.nativeViewProtected instanceof UIScrollView) {
view.off('scroll', onBackgroundViewScroll);
view.on('scroll', onBackgroundViewScroll);
adjustLayersForScrollView(view.nativeViewProtected);
}
}
function clearNonUniformColorBorders(nativeView) {
if (nativeView.borderLayer) {
nativeView.borderLayer.mask = null;
nativeView.borderLayer.sublayers = null;
}
nativeView.hasNonUniformBorderColor = false;
}
function clearNonUniformBorders(nativeView) {
if (nativeView.borderLayer) {
nativeView.borderLayer.removeFromSuperlayer();
nativeView.borderLayer = null;
}
nativeView.hasNonUniformBorder = false;
}
function parsePosition(pos) {
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(image, background, width, height) {
if (!image) {
return null;
}
const res = {
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 generatePatternImage(img, view, flip) {
const background = view.style.backgroundInternal;
const nativeView = view.nativeViewProtected;
if (!img || !nativeView) {
return null;
}
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 bgImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return flip ? _flipImage(bgImage) : bgImage;
}
// Flipping the default coordinate system
// https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GraphicsDrawingOverview/GraphicsDrawingOverview.html
function _flipImage(originalImage) {
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, total) {
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 getBorderCapRadius(a, b, c) {
return a && Math.min(a, Math.min(b, c));
}
function calculateNonUniformBorderCappedRadii(bounds, background) {
const { width, height } = bounds.size;
const { x, y } = bounds.origin;
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;
const cappedOuterRadii = {
topLeft: getBorderCapRadius(outerTopLeftRadius, (outerTopLeftRadius / topRadii) * width, (outerTopLeftRadius / leftRadii) * height),
topRight: getBorderCapRadius(outerTopRightRadius, (outerTopRightRadius / topRadii) * width, (outerTopRightRadius / rightRadii) * height),
bottomLeft: getBorderCapRadius(outerBottomLeftRadius, (outerBottomLeftRadius / bottomRadii) * width, (outerBottomLeftRadius / leftRadii) * height),
bottomRight: getBorderCapRadius(outerBottomRightRadius, (outerBottomRightRadius / bottomRadii) * width, (outerBottomRightRadius / rightRadii) * height),
};
return cappedOuterRadii;
}
function drawNonUniformBorders(nativeView, background) {
const layer = nativeView.layer;
const layerBounds = layer.bounds;
layer.borderColor = null;
layer.borderWidth = 0;
layer.cornerRadius = 0;
const cappedOuterRadii = calculateNonUniformBorderCappedRadii(layerBounds, background);
if (nativeView.maskType === iosViewUtils.LayerMask.BORDER && layer.mask instanceof CAShapeLayer) {
layer.mask.path = generateNonUniformBorderOuterClipPath(layerBounds, cappedOuterRadii);
}
if (background.hasBorderWidth()) {
if (!nativeView.hasNonUniformBorder) {
nativeView.borderLayer = CAShapeLayer.new();
nativeView.borderLayer.fillRule = kCAFillRuleEvenOdd;
layer.addSublayer(nativeView.borderLayer);
nativeView.hasNonUniformBorder = true;
}
if (background.hasUniformBorderColor()) {
// Use anti-aliasing or borders will draw incorrectly at times
nativeView.borderLayer.shouldRasterize = true;
nativeView.borderLayer.rasterizationScale = Screen.mainScreen.scale;
nativeView.borderLayer.fillColor = background.borderTopColor?.ios?.CGColor || UIColor.blackColor.CGColor;
nativeView.borderLayer.path = generateNonUniformBorderInnerClipPath(layerBounds, background, cappedOuterRadii);
}
else {
// Non-uniform borders need more layers in order to display multiple colors at the same time
let borderTopLayer, borderRightLayer, borderBottomLayer, borderLeftLayer;
if (!nativeView.hasNonUniformBorderColor) {
const maskLayer = CAShapeLayer.new();
maskLayer.fillRule = kCAFillRuleEvenOdd;
// Use anti-aliasing or borders will draw incorrectly at times
maskLayer.shouldRasterize = true;
maskLayer.rasterizationScale = Screen.mainScreen.scale;
nativeView.borderLayer.mask = maskLayer;
borderTopLayer = CAShapeLayer.new();
borderRightLayer = CAShapeLayer.new();
borderBottomLayer = CAShapeLayer.new();
borderLeftLayer = CAShapeLayer.new();
nativeView.borderLayer.addSublayer(borderTopLayer);
nativeView.borderLayer.addSublayer(borderRightLayer);
nativeView.borderLayer.addSublayer(borderBottomLayer);
nativeView.borderLayer.addSublayer(borderLeftLayer);
nativeView.hasNonUniformBorderColor = true;
}
else {
borderTopLayer = nativeView.borderLayer.sublayers[0];
borderRightLayer = nativeView.borderLayer.sublayers[1];
borderBottomLayer = nativeView.borderLayer.sublayers[2];
borderLeftLayer = nativeView.borderLayer.sublayers[3];
}
const paths = generateNonUniformMultiColorBorderPaths(layerBounds, background);
borderTopLayer.fillColor = background.borderTopColor?.ios?.CGColor || UIColor.blackColor.CGColor;
borderTopLayer.path = paths[0];
borderRightLayer.fillColor = background.borderRightColor?.ios?.CGColor || UIColor.blackColor.CGColor;
borderRightLayer.path = paths[1];
borderBottomLayer.fillColor = background.borderBottomColor?.ios?.CGColor || UIColor.blackColor.CGColor;
borderBottomLayer.path = paths[2];
borderLeftLayer.fillColor = background.borderLeftColor?.ios?.CGColor || UIColor.blackColor.CGColor;
borderLeftLayer.path = paths[3];
// Clip inner area to create borders
if (nativeView.borderLayer.mask instanceof CAShapeLayer) {
nativeView.borderLayer.mask.path = generateNonUniformBorderInnerClipPath(layerBounds, background, cappedOuterRadii);
}
}
}
}
function calculateInnerBorderClipRadius(radius, insetX, insetY) {
const innerXRadius = Math.max(0, radius - insetX);
const innerYRadius = Math.max(0, radius - insetY);
const innerMaxRadius = Math.max(innerXRadius, innerYRadius);
return {
xRadius: innerXRadius,
yRadius: innerYRadius,
maxRadius: innerMaxRadius,
};
}
/**
* Generates a path that represents the rounded view area.
*
* @param bounds
* @param cappedRadii
* @param offset
* @returns
*/
function generateNonUniformBorderOuterClipPath(bounds, cappedRadii, offset = 0) {
const { width, height } = bounds.size;
const { x, y } = bounds.origin;
const left = x - offset;
const top = y - offset;
const right = x + width + offset;
const bottom = y + height + offset;
const clipPath = CGPathCreateMutable();
CGPathMoveToPoint(clipPath, null, left + cappedRadii.topLeft, top);
CGPathAddArcToPoint(clipPath, null, right, top, right, top + cappedRadii.topRight, cappedRadii.topRight);
CGPathAddArcToPoint(clipPath, null, right, bottom, right - cappedRadii.bottomRight, bottom, cappedRadii.bottomRight);
CGPathAddArcToPoint(clipPath, null, left, bottom, left, bottom - cappedRadii.bottomLeft, cappedRadii.bottomLeft);
CGPathAddArcToPoint(clipPath, null, left, top, left + cappedRadii.topLeft, top, cappedRadii.topLeft);
CGPathCloseSubpath(clipPath);
return clipPath;
}
/**
* Generates a path that represents the area inside borders.
*
* @param bounds
* @param background
* @param cappedOuterRadii
* @returns
*/
function generateNonUniformBorderInnerClipPath(bounds, background, cappedOuterRadii) {
const { width, height } = bounds.size;
const { x, y } = bounds.origin;
const position = {
left: x,
top: y,
bottom: y + height,
right: x + width,
};
const borderTopWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderTopWidth));
const borderRightWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderRightWidth));
const borderBottomWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderBottomWidth));
const borderLeftWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderLeftWidth));
const borderVWidth = borderTopWidth + borderBottomWidth;
const borderHWidth = borderLeftWidth + borderRightWidth;
const cappedBorderTopWidth = borderTopWidth && borderTopWidth * Math.min(1, height / borderVWidth);
const cappedBorderRightWidth = borderRightWidth && borderRightWidth * Math.min(1, width / borderHWidth);
const cappedBorderBottomWidth = borderBottomWidth && borderBottomWidth * Math.min(1, height / borderVWidth);
const cappedBorderLeftWidth = borderLeftWidth && borderLeftWidth * Math.min(1, width / borderHWidth);
const clipPath = CGPathCreateMutable();
CGPathAddRect(clipPath, null, CGRectMake(x, y, width, height));
// Inner clip paths
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
CGPathMoveToPoint(clipPath, null, position.left + cappedOuterRadii.topLeft, position.top + cappedBorderTopWidth);
}
else {
CGPathMoveToPoint(clipPath, null, position.left, position.top);
}
if (cappedBorderTopWidth > 0 || cappedBorderRightWidth > 0) {
const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.topRight, cappedBorderRightWidth, cappedBorderTopWidth);
const innerTopRightTransform = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.right - cappedBorderRightWidth - xRadius, position.top + cappedBorderTopWidth + yRadius);
CGPathAddArc(clipPath, innerTopRightTransform, 0, 0, maxRadius, (Math.PI * 3) / 2, 0, false);
}
else {
CGPathAddLineToPoint(clipPath, null, position.right, position.top);
}
if (cappedBorderBottomWidth > 0 || cappedBorderRightWidth > 0) {
const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.bottomRight, cappedBorderRightWidth, cappedBorderBottomWidth);
const innerBottomRightTransform = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.right - cappedBorderRightWidth - xRadius, position.bottom - cappedBorderBottomWidth - yRadius);
CGPathAddArc(clipPath, innerBottomRightTransform, 0, 0, maxRadius, 0, Math.PI / 2, false);
}
else {
CGPathAddLineToPoint(clipPath, null, position.right, position.bottom);
}
if (cappedBorderBottomWidth > 0 || cappedBorderLeftWidth > 0) {
const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.bottomLeft, cappedBorderLeftWidth, cappedBorderBottomWidth);
const innerBottomLeftTransform = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.left + cappedBorderLeftWidth + xRadius, position.bottom - cappedBorderBottomWidth - yRadius);
CGPathAddArc(clipPath, innerBottomLeftTransform, 0, 0, maxRadius, Math.PI / 2, Math.PI, false);
}
else {
CGPathAddLineToPoint(clipPath, null, position.left, position.bottom);
}
if (cappedBorderTopWidth > 0 || cappedBorderLeftWidth > 0) {
const { xRadius, yRadius, maxRadius } = calculateInnerBorderClipRadius(cappedOuterRadii.topLeft, cappedBorderLeftWidth, cappedBorderTopWidth);
const innerTopLeftTransform = CGAffineTransformMake(maxRadius && xRadius / maxRadius, 0, 0, maxRadius && yRadius / maxRadius, position.left + cappedBorderLeftWidth + xRadius, position.top + cappedBorderTopWidth + yRadius);
CGPathAddArc(clipPath, innerTopLeftTransform, 0, 0, maxRadius, Math.PI, (Math.PI * 3) / 2, false);
}
else {
CGPathAddLineToPoint(clipPath, null, position.left, position.top);
}
CGPathCloseSubpath(clipPath);
return clipPath;
}
/**
* Calculates the needed widths for creating triangular shapes for each border.
* To achieve this, all border widths are scaled according to view bounds.
*
* @param bounds
* @param background
* @returns
*/
function getBorderTriangleWidths(bounds, background) {
const width = bounds.origin.x + bounds.size.width;
const height = bounds.origin.y + bounds.size.height;
const borderTopWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderTopWidth));
const borderRightWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderRightWidth));
const borderBottomWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderBottomWidth));
const borderLeftWidth = Math.max(0, layout.toDeviceIndependentPixels(background.borderLeftWidth));
const verticalBorderWidth = borderTopWidth + borderBottomWidth;
const horizontalBorderWidth = borderLeftWidth + borderRightWidth;
let verticalBorderMultiplier = verticalBorderWidth > 0 ? height / verticalBorderWidth : 0;
let horizontalBorderMultiplier = horizontalBorderWidth > 0 ? width / horizontalBorderWidth : 0;
// Both directions should consider each other in order to scale widths properly, as a view might have different width and height
if (verticalBorderMultiplier > 0 && verticalBorderMultiplier < horizontalBorderMultiplier) {
horizontalBorderMultiplier -= horizontalBorderMultiplier - verticalBorderMultiplier;
}
if (horizontalBorderMultiplier > 0 && horizontalBorderMultiplier < verticalBorderMultiplier) {
verticalBorderMultiplier -= verticalBorderMultiplier - horizontalBorderMultiplier;
}
return {
top: borderTopWidth * verticalBorderMultiplier,
right: borderRightWidth * horizontalBorderMultiplier,
bottom: borderBottomWidth * verticalBorderMultiplier,
left: borderLeftWidth * horizontalBorderMultiplier,
};
}
/**
* Generates paths for visualizing borders with different colors per side.
* This is achieved by extending all borders enough to consume entire view size,
* then using an even-odd inner mask to clip and eventually render borders according to their corresponding width.
*
* @param bounds
* @param background
* @returns
*/
function generateNonUniformMultiColorBorderPaths(bounds, background) {
const { width, height } = bounds.size;
const { x, y } = bounds.origin;
const position = {
left: x,
top: y,
bottom: y + height,
right: x + width,
};
const borderWidths = getBorderTriangleWidths(bounds, background);
const paths = new Array(4);
const lto = {
x: position.left,
y: position.top,
}; // left-top-outside
const lti = {
x: position.left + borderWidths.left,
y: position.top + borderWidths.top,
}; // left-top-inside
const rto = {
x: position.right,
y: position.top,
}; // right-top-outside
const rti = {
x: position.right - borderWidths.right,
y: position.top + borderWidths.top,
}; // right-top-inside
const rbo = {
x: position.right,
y: position.bottom,
}; // right-bottom-outside
const rbi = {
x: position.right - borderWidths.right,
y: position.bottom - borderWidths.bottom,
}; // right-bottom-inside
const lbo = {
x: position.left,
y: position.bottom,
}; // left-bottom-outside
const lbi = {
x: position.left + borderWidths.left,
y: position.bottom - borderWidths.bottom,
}; // left-bottom-inside
const borderTopColor = background.borderTopColor;
const borderRightColor = background.borderRightColor;
const borderBottomColor = background.borderBottomColor;
const borderLeftColor = background.borderLeftColor;
if (borderWidths.top > 0 && 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);
if (rti.x !== lti.x) {
CGPathAddLineToPoint(topBorderPath, null, lti.x, lti.y);
}
CGPathAddLineToPoint(topBorderPath, null, lto.x, lto.y);
paths[0] = topBorderPath;
}
if (borderWidths.right > 0 && 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);
if (rbi.y !== rti.y) {
CGPathAddLineToPoint(rightBorderPath, null, rti.x, rti.y);
}
CGPathAddLineToPoint(rightBorderPath, null, rto.x, rto.y);
paths[1] = rightBorderPath;
}
if (borderWidths.bottom > 0 && 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);
if (lbi.x !== rbi.x) {
CGPathAddLineToPoint(bottomBorderPath, null, rbi.x, rbi.y);
}
CGPathAddLineToPoint(bottomBorderPath, null, rbo.x, rbo.y);
paths[2] = bottomBorderPath;
}
if (borderWidths.left > 0 && 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);
if (lti.y !== lbi.y) {
CGPathAddLineToPoint(leftBorderPath, null, lbi.x, lbi.y);
}
CGPathAddLineToPoint(leftBorderPath, null, lbo.x, lbo.y);
paths[3] = leftBorderPath;
}
return paths;
}
function drawBoxShadow(view) {
const background = view.style.backgroundInternal;
const nativeView = view.nativeViewProtected;
const layer = nativeView.layer;
// There is no parent to add shadow to
if (!layer.superlayer) {
return;
}
const bounds = nativeView.bounds;
const boxShadow = background.getBoxShadow();
// Initialize outer shadows
let outerShadowContainerLayer;
if (nativeView.outerShadowContainerLayer) {
outerShadowContainerLayer = nativeView.outerShadowContainerLayer;
}
else {
outerShadowContainerLayer = CALayer.new();
// TODO: Make this dynamic when views get support for multiple shadows
const shadowLayer = CALayer.new();
// This mask is necessary to maintain transparent background
const maskLayer = CAShapeLayer.new();
maskLayer.fillRule = kCAFillRuleEvenOdd;
shadowLayer.mask = maskLayer;
outerShadowContainerLayer.addSublayer(shadowLayer);
// Instead of nesting it, add shadow container layer underneath view so that it's not affected by border masking
layer.superlayer.insertSublayerBelow(outerShadowContainerLayer, layer);
nativeView.outerShadowContainerLayer = outerShadowContainerLayer;
}
// Apply clip path to shadow
if (nativeView.maskType === iosViewUtils.LayerMask.CLIP_PATH && layer.mask instanceof CAShapeLayer) {
if (!outerShadowContainerLayer.mask) {
outerShadowContainerLayer.mask = CAShapeLayer.new();
}
if (outerShadowContainerLayer.mask instanceof CAShapeLayer) {
outerShadowContainerLayer.mask.path = layer.mask.path;
}
}
outerShadowContainerLayer.bounds = bounds;
outerShadowContainerLayer.transform = layer.transform;
outerShadowContainerLayer.anchorPoint = layer.anchorPoint;
outerShadowContainerLayer.position = nativeView.center;
outerShadowContainerLayer.zPosition = layer.zPosition;
// Inherit view visibility values
outerShadowContainerLayer.opacity = layer.opacity;
outerShadowContainerLayer.hidden = layer.hidden;
const outerShadowLayers = outerShadowContainerLayer.sublayers;
if (outerShadowLayers?.count) {
for (let i = 0, count = outerShadowLayers.count; i < count; i++) {
const shadowLayer = outerShadowLayers[i];
const shadowRadius = layout.toDeviceIndependentPixels(boxShadow.blurRadius);
const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
const offsetX = layout.toDeviceIndependentPixels(boxShadow.offsetX);
const offsetY = layout.toDeviceIndependentPixels(boxShadow.offsetY);
const { maskPath, shadowPath } = ios.generateShadowLayerPaths(view, bounds);
shadowLayer.allowsEdgeAntialiasing = true;
shadowLayer.contentsScale = Screen.mainScreen.scale;
// Shadow opacity is handled on the shadow's color instance
shadowLayer.shadowOpacity = boxShadow.color?.a ? boxShadow.color.a / 255 : 1;
shadowLayer.shadowRadius = shadowRadius;
shadowLayer.shadowColor = boxShadow.color?.ios?.CGColor;
shadowLayer.shadowOffset = CGSizeMake(offsetX, offsetY);
// Apply spread radius by expanding shadow layer bounds (this has a nice glow with radii set to 0)
shadowLayer.shadowPath = shadowPath;
// A mask that ensures that view maintains transparent background
if (shadowLayer.mask instanceof CAShapeLayer) {
shadowLayer.mask.path = maskPath;
}
}
}
}
function clearBoxShadow(nativeView) {
if (nativeView.outerShadowContainerLayer) {
nativeView.outerShadowContainerLayer.removeFromSuperlayer();
nativeView.outerShadowContainerLayer = null;
}
}
/**
* Creates a mask that ensures no shadow will be displayed underneath transparent backgrounds.
*
* @param bounds
* @param boxShadow
* @param bordersClipPath
* @returns
*/
function generateShadowMaskPath(bounds, boxShadow, innerClipPath) {
const shadowRadius = layout.toDeviceIndependentPixels(boxShadow.blurRadius);
const spreadRadius = layout.toDeviceIndependentPixels(boxShadow.spreadRadius);
const offsetX = layout.toDeviceIndependentPixels(boxShadow.offsetX);
const offsetY = layout.toDeviceIndependentPixels(boxShadow.offsetY);
// This value has to be large enough to avoid clipping shadow halo effect
const outerRectRadius = shadowRadius * 3 + spreadRadius;
const maskPath = CGPathCreateMutable();
// Proper clip position and size
const outerRect = CGRectOffset(CGRectInset(bounds, -outerRectRadius, -outerRectRadius), offsetX, offsetY);
CGPathAddPath(maskPath, null, innerClipPath);
CGPathAddRect(maskPath, null, outerRect);
return maskPath;
}
function rectPath(value, position) {
const arr = value.split(/[\s]+/);
const top = cssValueToDeviceIndependentPixels(arr[0], position.top);
const right = cssValueToDeviceIndependentPixels(arr[1], position.right);
const bottom = cssValueToDeviceIndependentPixels(arr[2], position.bottom);
const left = cssValueToDeviceIndependentPixels(arr[3], position.left);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
function insetPath(value, position) {
const arr = value.split(/[\s]+/);
let topString;
let rightString;
let bottomString;
let leftString;
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, position.bottom);
const right = cssValueToDeviceIndependentPixels('100%', position.right) - cssValueToDeviceIndependentPixels(rightString, position.right);
const bottom = cssValueToDeviceIndependentPixels('100%', position.bottom) - cssValueToDeviceIndependentPixels(bottomString, position.bottom);
const left = cssValueToDeviceIndependentPixels(leftString, position.right);
return UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right - left, bottom - top)).CGPath;
}
function circlePath(value, position) {
const arr = value.split(/[\s]+/);
const radius = cssValueToDeviceIndependentPixels(arr[0], (position.right > position.bottom ? position.bottom : position.right) / 2);
const y = cssValueTo