diff --git a/apps/tests/xml-declaration/mainPage.xml b/apps/tests/xml-declaration/mainPage.xml
index 9f7b9cb3e..ecb6f28c7 100644
--- a/apps/tests/xml-declaration/mainPage.xml
+++ b/apps/tests/xml-declaration/mainPage.xml
@@ -85,4 +85,4 @@
-
+
\ No newline at end of file
diff --git a/apps/ui-tests-app/css/clip-path.xml b/apps/ui-tests-app/css/clip-path.xml
new file mode 100644
index 000000000..33a64e9d9
--- /dev/null
+++ b/apps/ui-tests-app/css/clip-path.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/core/view.android.ts b/ui/core/view.android.ts
index 0bd4a7431..d46bbb069 100644
--- a/ui/core/view.android.ts
+++ b/ui/core/view.android.ts
@@ -742,6 +742,7 @@ export class ViewStyler implements style.Styler {
style.registerHandler(style.borderWidthProperty, borderHandler);
style.registerHandler(style.borderColorProperty, borderHandler);
style.registerHandler(style.borderRadiusProperty, borderHandler);
+ style.registerHandler(style.clipPathProperty, borderHandler);
style.registerHandler(style.nativeLayoutParamsProperty, new style.StylePropertyChangedHandler(
ViewStyler.setNativeLayoutParamsProperty,
diff --git a/ui/core/view.ios.ts b/ui/core/view.ios.ts
index 948f57eb6..e889ac946 100644
--- a/ui/core/view.ios.ts
+++ b/ui/core/view.ios.ts
@@ -567,7 +567,7 @@ export class ViewStyler implements style.Styler {
private static getTranslateYProperty(view: View): any {
return view.translateY;
}
-
+
//z-index
private static setZIndexProperty(view: View, newValue: any) {
view.ios.layer.zPosition = newValue;
@@ -580,13 +580,41 @@ export class ViewStyler implements style.Styler {
private static getZIndexProperty(view: View): any {
return view.ios.layer.zPosition;
}
+
+ //Clip-path methods
+ private static setClipPathProperty(view: View, newValue: any) {
+ var nativeView: UIView = view._nativeView;
+ if (nativeView) {
+ ensureBackground();
+ var updateSuspended = view._isPresentationLayerUpdateSuspeneded();
+ if (!updateSuspended) {
+ CATransaction.begin();
+ }
+ nativeView.backgroundColor = background.ios.createBackgroundUIColor(view);
+ if (!updateSuspended) {
+ CATransaction.commit();
+ }
+ }
+ }
+
+ private static resetClipPathProperty(view: View, nativeValue: any) {
+ var nativeView: UIView = view._nativeView;
+ if (nativeView) {
+ // TODO: Check how to reset.
+ }
+ }
public static registerHandlers() {
+
style.registerHandler(style.backgroundInternalProperty, new style.StylePropertyChangedHandler(
ViewStyler.setBackgroundInternalProperty,
ViewStyler.resetBackgroundInternalProperty,
ViewStyler.getNativeBackgroundInternalValue));
+ style.registerHandler(style.clipPathProperty, new style.StylePropertyChangedHandler(
+ ViewStyler.setClipPathProperty,
+ ViewStyler.resetClipPathProperty));
+
style.registerHandler(style.visibilityProperty, new style.StylePropertyChangedHandler(
ViewStyler.setVisibilityProperty,
ViewStyler.resetVisibilityProperty));
diff --git a/ui/styling/background-common.ts b/ui/styling/background-common.ts
index d9eae8468..228260930 100644
--- a/ui/styling/background-common.ts
+++ b/ui/styling/background-common.ts
@@ -3,6 +3,7 @@ import colorModule = require("color");
import enums = require("ui/enums");
import definition = require("ui/styling/background");
import cssValue = require("css-value");
+import utils = require("utils/utils");
import * as typesModule from "utils/types";
var types: typeof typesModule;
@@ -242,3 +243,18 @@ export class Background implements definition.Background {
colorModule.Color.equals(value1.color, value2.color);
}
}
+
+export function cssValueToDevicePixels(source: string, total: number): number {
+ var result;
+ source = source.trim();
+
+ if (source.indexOf("px") !== -1) {
+ result = parseFloat(source.replace("px", ""));
+ }
+ else if (source.indexOf("%") !== -1 && total > 0) {
+ result = (parseFloat(source.replace("%", "")) / 100) * utils.layout.toDeviceIndependentPixels(total);
+ } else {
+ result = parseFloat(source);
+ }
+ return utils.layout.toDevicePixels(result);
+}
\ No newline at end of file
diff --git a/ui/styling/background.android.ts b/ui/styling/background.android.ts
index 38babad12..ed67b01bb 100644
--- a/ui/styling/background.android.ts
+++ b/ui/styling/background.android.ts
@@ -42,12 +42,23 @@ export module ad {
private _borderWidth: number;
private _cornerRadius: number;
private _borderColor: number;
+ private _clipPath: string;
constructor() {
super();
return global.__native(this);
}
+ get clipPath(): string {
+ return this._clipPath;
+ }
+ set clipPath(value: string) {
+ if (this._clipPath !== value) {
+ this._clipPath = value;
+ this.invalidateSelf();
+ }
+ }
+
get borderWidth(): number {
return this._borderWidth;
}
@@ -101,7 +112,7 @@ export module ad {
let backgroundBoundsF = new android.graphics.RectF(bounds.left + backoffAntialias, bounds.top + backoffAntialias, bounds.right - backoffAntialias, bounds.bottom - backoffAntialias);
let outerRadius = this._cornerRadius * this._density;
-
+
// draw background
if (this.background.color && this.background.color.android) {
let backgroundColorPaint = new android.graphics.Paint();
@@ -109,7 +120,11 @@ export module ad {
backgroundColorPaint.setColor(this.background.color.android);
backgroundColorPaint.setAntiAlias(true);
- canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundColorPaint);
+ if (this.clipPath) {
+ drawClipPath(this.clipPath, canvas, backgroundColorPaint, backgroundBoundsF);
+ } else {
+ canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundColorPaint);
+ }
}
// draw image
@@ -139,21 +154,25 @@ export module ad {
params.posX = params.repeatX ? 0 : params.posX;
params.posY = params.repeatY ? 0 : params.posY;
- let supportsPathOp = android.os.Build.VERSION.SDK_INT >= 19;
- if (supportsPathOp) {
- // Path.Op can be used in API level 19+ to achieve the perfect geometry.
- let backgroundPath = new android.graphics.Path();
- backgroundPath.addRoundRect(backgroundBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
- let backgroundNoRepeatPath = new android.graphics.Path();
- backgroundNoRepeatPath.addRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight, android.graphics.Path.Direction.CCW);
- (backgroundPath).op(backgroundNoRepeatPath, (android).graphics.Path.Op.INTERSECT);
- canvas.drawPath(backgroundPath, backgroundImagePaint);
+ if (this.clipPath) {
+ drawClipPath(this.clipPath, canvas, backgroundImagePaint, backgroundBoundsF);
} else {
- // Clipping here will not be antialiased but at least it won't shine through the rounded corners.
- canvas.save();
- canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight);
- canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundImagePaint);
- canvas.restore();
+ let supportsPathOp = android.os.Build.VERSION.SDK_INT >= 19;
+ if (supportsPathOp) {
+ // Path.Op can be used in API level 19+ to achieve the perfect geometry.
+ let backgroundPath = new android.graphics.Path();
+ backgroundPath.addRoundRect(backgroundBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
+ let backgroundNoRepeatPath = new android.graphics.Path();
+ backgroundNoRepeatPath.addRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight, android.graphics.Path.Direction.CCW);
+ (backgroundPath).op(backgroundNoRepeatPath, (android).graphics.Path.Op.INTERSECT);
+ canvas.drawPath(backgroundPath, backgroundImagePaint);
+ } else {
+ // Clipping here will not be antialiased but at least it won't shine through the rounded corners.
+ canvas.save();
+ canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight);
+ canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundImagePaint);
+ canvas.restore();
+ }
}
}
@@ -164,23 +183,29 @@ export module ad {
borderPaint.setColor(this._borderColor);
borderPaint.setAntiAlias(true);
- if (outerRadius <= 0) {
+ if (this.clipPath) {
borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
borderPaint.setStrokeWidth(borderWidth);
- canvas.drawRect(middleBoundsF, borderPaint);
- } else if (outerRadius >= borderWidth) {
- borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
- borderPaint.setStrokeWidth(borderWidth);
- let middleRadius = Math.max(0, outerRadius - halfBorderWidth);
- canvas.drawRoundRect(middleBoundsF, middleRadius, middleRadius, borderPaint);
+ drawClipPath(this.clipPath, canvas, borderPaint, backgroundBoundsF);
} else {
- let borderPath = new android.graphics.Path();
- let borderOuterBoundsF = new android.graphics.RectF(bounds.left, bounds.top, bounds.right, bounds.bottom);
- borderPath.addRoundRect(borderOuterBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
- let borderInnerBoundsF = new android.graphics.RectF(bounds.left + borderWidth, bounds.top + borderWidth, bounds.right - borderWidth, bounds.bottom - borderWidth);
- borderPath.addRect(borderInnerBoundsF, android.graphics.Path.Direction.CW);
- borderPaint.setStyle(android.graphics.Paint.Style.FILL);
- canvas.drawPath(borderPath, borderPaint);
+ if (outerRadius <= 0) {
+ borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
+ borderPaint.setStrokeWidth(borderWidth);
+ canvas.drawRect(middleBoundsF, borderPaint);
+ } else if (outerRadius >= borderWidth) {
+ borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
+ borderPaint.setStrokeWidth(borderWidth);
+ let middleRadius = Math.max(0, outerRadius - halfBorderWidth);
+ canvas.drawRoundRect(middleBoundsF, middleRadius, middleRadius, borderPaint);
+ } else {
+ let borderPath = new android.graphics.Path();
+ let borderOuterBoundsF = new android.graphics.RectF(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ borderPath.addRoundRect(borderOuterBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
+ let borderInnerBoundsF = new android.graphics.RectF(bounds.left + borderWidth, bounds.top + borderWidth, bounds.right - borderWidth, bounds.bottom - borderWidth);
+ borderPath.addRect(borderInnerBoundsF, android.graphics.Path.Direction.CW);
+ borderPaint.setStyle(android.graphics.Paint.Style.FILL);
+ canvas.drawPath(borderPath, borderPaint);
+ }
}
}
}
@@ -209,6 +234,8 @@ export module ad {
ensureBorderDrawable();
ensureLazyRequires();
+ var clipPathValue = v.style._getValue(style.clipPathProperty);
+
var backgroundValue = v.style._getValue(style.backgroundInternalProperty);
var borderWidth = v.borderWidth;
var bkg = nativeView.getBackground();
@@ -220,7 +247,7 @@ export module ad {
let backgroundColor = bkg.backgroundColor = v.style._getValue(style.backgroundColorProperty).android;
bkg.setColorFilter(backgroundColor, android.graphics.PorterDuff.Mode.SRC_IN);
bkg.backgroundColor = backgroundColor;
- } else if (v.borderWidth !== 0 || v.borderRadius !== 0 || !backgroundValue.isEmpty()) {
+ } else if (v.borderWidth !== 0 || v.borderRadius !== 0 || !backgroundValue.isEmpty() || !clipPathValue.isEmpty()) {
if (!(bkg instanceof BorderDrawableClass)) {
bkg = new BorderDrawableClass();
@@ -236,6 +263,7 @@ export module ad {
bkg.cornerRadius = v.borderRadius;
bkg.borderColor = v.borderColor ? v.borderColor.android : android.graphics.Color.TRANSPARENT;
bkg.background = backgroundValue;
+ bkg.clipPath = clipPathValue;
if (getSDK() < 18) {
// Switch to software because of unsupported canvas methods if hardware acceleration is on:
@@ -274,3 +302,66 @@ export module ad {
);
}
}
+
+function drawClipPath(clipPath: string, canvas: android.graphics.Canvas, paint: android.graphics.Paint, bounds: android.graphics.RectF) {
+ var functionName = clipPath.substring(0, clipPath.indexOf("("));
+ var value = clipPath.replace(`${functionName}(`, "").replace(")", "");
+
+ if (functionName === "rect") {
+ var arr = value.split(/[\s]+/);
+
+ var top = common.cssValueToDevicePixels(arr[0], bounds.top);
+ var left = common.cssValueToDevicePixels(arr[1], bounds.left);
+ var bottom = common.cssValueToDevicePixels(arr[2], bounds.bottom);
+ var right = common.cssValueToDevicePixels(arr[3], bounds.right);
+
+ canvas.drawRect(left, top, right, bottom, paint);
+
+ } else if (functionName === "circle") {
+ var arr = value.split(/[\s]+/);
+
+ var radius = common.cssValueToDevicePixels(arr[0], (bounds.width() > bounds.height() ? bounds.height() : bounds.width()) / 2);
+ var y = common.cssValueToDevicePixels(arr[2], bounds.height());
+ var x = common.cssValueToDevicePixels(arr[3], bounds.width());
+
+ canvas.drawCircle(x, y, radius, paint);
+
+ } else if (functionName === "ellipse") {
+ var arr = value.split(/[\s]+/);
+
+ var rX = common.cssValueToDevicePixels(arr[0], bounds.right);
+ var rY = common.cssValueToDevicePixels(arr[1], bounds.bottom);
+ var cX = common.cssValueToDevicePixels(arr[3], bounds.right);
+ var cY = common.cssValueToDevicePixels(arr[4], bounds.bottom);
+
+ var left = cX - rX;
+ var top = cY - rY;
+ var right = (rX * 2) + left;
+ var bottom = (rY * 2) + top;
+
+ canvas.drawOval(new android.graphics.RectF(left, top, right, bottom), paint);
+
+ } else if (functionName === "polygon") {
+ var path = new android.graphics.Path();
+ var firstPoint: view.Point;
+ var arr = value.split(/[,]+/);
+ for (let i = 0; i < arr.length; i++) {
+ let xy = arr[i].trim().split(/[\s]+/);
+ let point: view.Point = {
+ x: common.cssValueToDevicePixels(xy[0], bounds.width()),
+ y: common.cssValueToDevicePixels(xy[1], bounds.height())
+ };
+
+ if (!firstPoint) {
+ firstPoint = point;
+ path.moveTo(point.x, point.y);
+ }
+
+ path.lineTo(point.x, point.y);
+ }
+
+ path.lineTo(firstPoint.x, firstPoint.y);
+
+ canvas.drawPath(path, paint);
+ }
+}
\ No newline at end of file
diff --git a/ui/styling/background.ios.ts b/ui/styling/background.ios.ts
index 6dcff162a..c657a4a89 100644
--- a/ui/styling/background.ios.ts
+++ b/ui/styling/background.ios.ts
@@ -13,11 +13,16 @@ function ensureStyle() {
export module ios {
export function createBackgroundUIColor(view: viewModule.View, flip?: boolean): UIColor {
- if(!view._nativeView){
+ if (!view._nativeView) {
return undefined;
}
ensureStyle();
- var background = view.style._getValue(style.backgroundInternalProperty);
+
+ if (view.style.clipPath) {
+ drawClipPath(view);
+ }
+
+ var background = view.style._getValue(style.backgroundInternalProperty);
if (!background || background.isEmpty()) {
return undefined;
@@ -98,3 +103,103 @@ export module ios {
return flippedImage;
}
}
+
+function drawClipPath(view: viewModule.View) {
+ var path: any;
+
+ var nativeView = view._nativeView;
+ var bounds = {
+ left: nativeView.bounds.origin.x,
+ top: nativeView.bounds.origin.y,
+ bottom: nativeView.bounds.size.height,
+ right: nativeView.bounds.size.width
+ };
+
+ if (bounds.right === 0 || bounds.bottom === 0) {
+ return;
+ }
+
+ var clipPath = view.style.clipPath;
+
+ var functionName = clipPath.substring(0, clipPath.indexOf("("));
+ var value = clipPath.replace(`${functionName}(`, "").replace(")", "");
+
+ if (functionName === "rect") {
+ var arr = value.split(/[\s]+/);
+
+ var top = common.cssValueToDevicePixels(arr[0], bounds.top);
+ var left = common.cssValueToDevicePixels(arr[1], bounds.left);
+ var bottom = common.cssValueToDevicePixels(arr[2], bounds.bottom);
+ var right = common.cssValueToDevicePixels(arr[3], bounds.right);
+
+ path = UIBezierPath.bezierPathWithRect(CGRectMake(left, top, right, bottom)).CGPath;
+
+ } else if (functionName === "circle") {
+ var arr = value.split(/[\s]+/);
+
+ var radius = common.cssValueToDevicePixels(arr[0], (bounds.right > bounds.bottom ? bounds.bottom : bounds.right) / 2);
+ var y = common.cssValueToDevicePixels(arr[2], bounds.bottom);
+ var x = common.cssValueToDevicePixels(arr[3], bounds.right);
+
+ path = UIBezierPath.bezierPathWithArcCenterRadiusStartAngleEndAngleClockwise(CGPointMake(x, y), radius, 0, 360, true).CGPath;
+
+ } else if (functionName === "ellipse") {
+ var arr = value.split(/[\s]+/);
+
+ var rX = common.cssValueToDevicePixels(arr[0], bounds.right);
+ var rY = common.cssValueToDevicePixels(arr[1], bounds.bottom);
+ var cX = common.cssValueToDevicePixels(arr[3], bounds.right);
+ var cY = common.cssValueToDevicePixels(arr[4], bounds.bottom);
+
+ var left = cX - rX;
+ var top = cY - rY;
+ var width = rX * 2;
+ var height = rY * 2;
+
+ path = UIBezierPath.bezierPathWithOvalInRect(CGRectMake(left, top, width, height)).CGPath;
+
+ } else if (functionName === "polygon") {
+
+ path = CGPathCreateMutable()
+
+ var firstPoint: viewModule.Point;
+ var arr = value.split(/[,]+/);
+ for (let i = 0; i < arr.length; i++) {
+ let xy = arr[i].trim().split(/[\s]+/);
+ let point: viewModule.Point = {
+ x: common.cssValueToDevicePixels(xy[0], bounds.right),
+ y: common.cssValueToDevicePixels(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)
+ }
+
+ if (path) {
+ var shape = CAShapeLayer.layer();
+ shape.path = path;
+ nativeView.layer.mask = shape;
+ nativeView.clipsToBounds = true;
+
+ if (view.borderWidth > 0 && view.borderColor) {
+ var borderLayer = CAShapeLayer.layer();
+ borderLayer.path = path;
+ borderLayer.lineWidth = view.borderWidth * 2;
+ borderLayer.strokeColor = view.borderColor.ios.CGColor;
+ borderLayer.fillColor = UIColor.clearColor().CGColor;
+
+ borderLayer.frame = nativeView.bounds;
+
+ nativeView.layer.borderColor = undefined;
+ nativeView.layer.borderWidth = 0;
+ nativeView.layer.addSublayer(borderLayer);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/styling/style.d.ts b/ui/styling/style.d.ts
index 583be9c5f..dcbc3b192 100644
--- a/ui/styling/style.d.ts
+++ b/ui/styling/style.d.ts
@@ -75,6 +75,7 @@ declare module "ui/styling/style" {
public horizontalAlignment: string;
public verticalAlignment: string;
public visibility: string;
+ public clipPath: string;
public opacity: number;
public whiteSpace: string;
public letterSpacing: number;
@@ -111,6 +112,7 @@ declare module "ui/styling/style" {
export var borderColorProperty: styleProperty.Property;
export var borderWidthProperty: styleProperty.Property;
export var borderRadiusProperty: styleProperty.Property;
+ export var clipPathProperty: styleProperty.Property;
export var backgroundInternalProperty: styleProperty.Property;
export var fontSizeProperty: styleProperty.Property;
export var fontFamilyProperty: styleProperty.Property;
diff --git a/ui/styling/style.ts b/ui/styling/style.ts
index 15d2a11d6..0e53be666 100644
--- a/ui/styling/style.ts
+++ b/ui/styling/style.ts
@@ -403,6 +403,13 @@ function isPaddingValid(value: number): boolean {
return isFinite(value) && !isNaN(value) && value >= 0;
}
+var supportedPaths = ["rect", "circle", "ellipse", "polygon"];
+function isClipPathValid(value: string): boolean {
+ var functionName = value.substring(0, value.indexOf("(")).trim();
+
+ return supportedPaths.indexOf(functionName) !== -1 || value === "";
+}
+
function isMarginValid(value: number): boolean {
var result = convertToPercentHelper(value);
if (result.isError) {
@@ -590,6 +597,13 @@ export class Style extends DependencyObservable implements styling.Style {
this._setValue(borderRadiusProperty, value);
}
+ get clipPath(): string {
+ return this._getValue(clipPathProperty);
+ }
+ set clipPath(value: string) {
+ this._setValue(clipPathProperty, value);
+ }
+
get fontSize(): number {
return this._getValue(fontSizeProperty);
}
@@ -880,6 +894,11 @@ export class Style extends DependencyObservable implements styling.Style {
if (!(this._getValue(backgroundInternalProperty)).isEmpty()) {
this._applyProperty(backgroundInternalProperty, this._getValue(backgroundInternalProperty));
}
+
+ var clipPathPropertyValue = this._getValue(clipPathProperty);
+ if (types.isString(clipPathPropertyValue) && clipPathPropertyValue !== "") {
+ this._applyProperty(clipPathProperty, clipPathPropertyValue);
+ }
}
private _applyProperty(property: Property, newValue: any) {
@@ -1048,6 +1067,9 @@ export var borderWidthProperty = new styleProperty.Property("borderWidth", "bord
export var borderRadiusProperty = new styleProperty.Property("borderRadius", "border-radius",
new PropertyMetadata(0, AffectsLayout, null, isPaddingValid), converters.numberConverter);
+
+export var clipPathProperty = new styleProperty.Property("clipPath", "clip-path",
+ new PropertyMetadata(undefined, AffectsLayout, null, isClipPathValid));
export var backgroundInternalProperty = new styleProperty.Property("_backgroundInternal", "_backgroundInternal",
new PropertyMetadata(background.Background.default, PropertyMetadataSettings.None, undefined, undefined, background.Background.equals));
diff --git a/ui/styling/styling.d.ts b/ui/styling/styling.d.ts
index e47395a84..280111f2f 100644
--- a/ui/styling/styling.d.ts
+++ b/ui/styling/styling.d.ts
@@ -82,6 +82,11 @@
* Gets or sets the border-radius style property.
*/
borderRadius: number;
+
+ /**
+ * Gets or sets the clip-path style property.
+ */
+ clipPath: string;
/**
* Gets or sets font-size style property.