mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 19:26:42 +08:00

* feat(css-bkg-pos): Added possibility to declare background pos by single numeric value * feat(css-bkg-pos): Implemented numeric bkg pos for iOS * feat(css-bkg-pos): removed unnecessary code Co-authored-by: Vasil Trifonov <v.trifonov@gmail.com>
909 lines
36 KiB
TypeScript
909 lines
36 KiB
TypeScript
import { ScrollEventData } from "../scroll-view";
|
|
|
|
import { Background as BackgroundDefinition } from "./background";
|
|
import { View, Point } from "../core/view";
|
|
import { LinearGradient } from "./linear-gradient";
|
|
import { Color } from "../../color";
|
|
import { isDataURI, isFileOrResourcePath, layout } from "../../utils/utils";
|
|
import { ImageSource } from "../../image-source";
|
|
import { CSSValue, parse as cssParse } from "../../css-value";
|
|
|
|
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;
|
|
}
|
|
|
|
interface Rect {
|
|
left: number;
|
|
top: number;
|
|
right: number;
|
|
bottom: number;
|
|
}
|
|
|
|
const clearCGColor = UIColor.clearColor.CGColor;
|
|
const symbolUrl = Symbol("backgroundImageUrl");
|
|
|
|
export module ios {
|
|
export function createBackgroundUIColor(view: View, callback: (uiColor: UIColor) => void, flip?: boolean): void {
|
|
const background = view.style.backgroundInternal;
|
|
const nativeView = <NativeView>view.nativeViewProtected;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 & 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(<any>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: RegExp = /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;
|
|
}
|
|
|
|
function drawGradient(nativeView: NativeView, gradient: LinearGradient) {
|
|
|
|
const gradientLayer = CAGradientLayer.layer();
|
|
gradientLayer.frame = nativeView.bounds;
|
|
nativeView.gradientLayer = gradientLayer;
|
|
|
|
const iosColors = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length);
|
|
const iosStops = NSMutableArray.alloc<number>().initWithCapacity(gradient.colorStops.length);
|
|
let hasStops = false;
|
|
|
|
gradient.colorStops.forEach(stop => {
|
|
iosColors.addObject(stop.color.ios.CGColor);
|
|
if (stop.offset) {
|
|
iosStops.addObject(stop.offset.value);
|
|
hasStops = true;
|
|
}
|
|
});
|
|
|
|
gradientLayer.colors = iosColors;
|
|
|
|
if (hasStops) {
|
|
gradientLayer.locations = iosStops;
|
|
}
|
|
|
|
const alpha = gradient.angle / (Math.PI * 2);
|
|
const startX = Math.pow(
|
|
Math.sin(Math.PI * (alpha + 0.75)),
|
|
2
|
|
);
|
|
const startY = Math.pow(
|
|
Math.sin(Math.PI * (alpha + 0.5)),
|
|
2
|
|
);
|
|
const endX = Math.pow(
|
|
Math.sin(Math.PI * (alpha + 0.25)),
|
|
2
|
|
);
|
|
const endY = Math.pow(
|
|
Math.sin(Math.PI * alpha),
|
|
2
|
|
);
|
|
gradientLayer.startPoint = { x: startX, y: startY };
|
|
gradientLayer.endPoint = { x: endX, y: endY };
|
|
|
|
nativeView.layer.insertSublayerAtIndex(gradientLayer, 0);
|
|
|
|
}
|
|
|
|
function clearGradient(nativeView: NativeView): void {
|
|
if (nativeView.gradientLayer) {
|
|
nativeView.gradientLayer.removeFromSuperlayer();
|
|
}
|
|
}
|
|
|
|
function drawClipPath(nativeView: UIView, background: BackgroundDefinition) {
|
|
const layer = nativeView.layer;
|
|
const layerBounds = layer.bounds;
|
|
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;
|
|
}
|