Skip to content

Commit b8785af

Browse files
committed
Merge pull request NativeScript#2102 from NativeScript/clip-path
clip-path support added
2 parents 16e3185 + ebed79d commit b8785af

File tree

10 files changed

+314
-35
lines changed

10 files changed

+314
-35
lines changed

apps/tests/xml-declaration/mainPage.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,4 @@
8585
</TabViewItem>
8686
</TabView.items>
8787
</TabView>
88-
</Page>
88+
</Page>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Page>
2+
<GridLayout rows="*,*,*,*,*">
3+
<Button text="circle" style="background-color: red; border-width:2; border-color: blue; clip-path: circle(100% at 50% 50%);"/>
4+
<Button text="ellipse" style="background-color: orange; border-width:2; border-color: green; clip-path: ellipse(50% 50% at 50% 50%);" row="1" />
5+
<Button text="rect" style="background-color: red; border-width:2; border-color: blue; clip-path: rect(0, 0, 100%, 100%);" row="2" />
6+
<Button text="polygon" style="background-color: magenta; border-width:2; border-color: red; clip-path: polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%);" row="3" />
7+
<Button text="image polygon" style="background-image: url('https://media-cdn.tripadvisor.com/media/photo-s/03/9b/2f/ce/maldives.jpg'); border-width:2; border-color: red; clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);" row="4" />
8+
</GridLayout>
9+
</Page>

ui/core/view.android.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ export class ViewStyler implements style.Styler {
742742
style.registerHandler(style.borderWidthProperty, borderHandler);
743743
style.registerHandler(style.borderColorProperty, borderHandler);
744744
style.registerHandler(style.borderRadiusProperty, borderHandler);
745+
style.registerHandler(style.clipPathProperty, borderHandler);
745746

746747
style.registerHandler(style.nativeLayoutParamsProperty, new style.StylePropertyChangedHandler(
747748
ViewStyler.setNativeLayoutParamsProperty,

ui/core/view.ios.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ export class ViewStyler implements style.Styler {
567567
private static getTranslateYProperty(view: View): any {
568568
return view.translateY;
569569
}
570-
570+
571571
//z-index
572572
private static setZIndexProperty(view: View, newValue: any) {
573573
view.ios.layer.zPosition = newValue;
@@ -580,13 +580,41 @@ export class ViewStyler implements style.Styler {
580580
private static getZIndexProperty(view: View): any {
581581
return view.ios.layer.zPosition;
582582
}
583+
584+
//Clip-path methods
585+
private static setClipPathProperty(view: View, newValue: any) {
586+
var nativeView: UIView = <UIView>view._nativeView;
587+
if (nativeView) {
588+
ensureBackground();
589+
var updateSuspended = view._isPresentationLayerUpdateSuspeneded();
590+
if (!updateSuspended) {
591+
CATransaction.begin();
592+
}
593+
nativeView.backgroundColor = background.ios.createBackgroundUIColor(view);
594+
if (!updateSuspended) {
595+
CATransaction.commit();
596+
}
597+
}
598+
}
599+
600+
private static resetClipPathProperty(view: View, nativeValue: any) {
601+
var nativeView: UIView = <UIView>view._nativeView;
602+
if (nativeView) {
603+
// TODO: Check how to reset.
604+
}
605+
}
583606

584607
public static registerHandlers() {
608+
585609
style.registerHandler(style.backgroundInternalProperty, new style.StylePropertyChangedHandler(
586610
ViewStyler.setBackgroundInternalProperty,
587611
ViewStyler.resetBackgroundInternalProperty,
588612
ViewStyler.getNativeBackgroundInternalValue));
589613

614+
style.registerHandler(style.clipPathProperty, new style.StylePropertyChangedHandler(
615+
ViewStyler.setClipPathProperty,
616+
ViewStyler.resetClipPathProperty));
617+
590618
style.registerHandler(style.visibilityProperty, new style.StylePropertyChangedHandler(
591619
ViewStyler.setVisibilityProperty,
592620
ViewStyler.resetVisibilityProperty));

ui/styling/background-common.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import colorModule = require("color");
33
import enums = require("ui/enums");
44
import definition = require("ui/styling/background");
55
import cssValue = require("css-value");
6+
import utils = require("utils/utils");
67
import * as typesModule from "utils/types";
78

89
var types: typeof typesModule;
@@ -242,3 +243,18 @@ export class Background implements definition.Background {
242243
colorModule.Color.equals(value1.color, value2.color);
243244
}
244245
}
246+
247+
export function cssValueToDevicePixels(source: string, total: number): number {
248+
var result;
249+
source = source.trim();
250+
251+
if (source.indexOf("px") !== -1) {
252+
result = parseFloat(source.replace("px", ""));
253+
}
254+
else if (source.indexOf("%") !== -1 && total > 0) {
255+
result = (parseFloat(source.replace("%", "")) / 100) * utils.layout.toDeviceIndependentPixels(total);
256+
} else {
257+
result = parseFloat(source);
258+
}
259+
return utils.layout.toDevicePixels(result);
260+
}

ui/styling/background.android.ts

Lines changed: 122 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,23 @@ export module ad {
4242
private _borderWidth: number;
4343
private _cornerRadius: number;
4444
private _borderColor: number;
45+
private _clipPath: string;
4546

4647
constructor() {
4748
super();
4849
return global.__native(this);
4950
}
5051

52+
get clipPath(): string {
53+
return this._clipPath;
54+
}
55+
set clipPath(value: string) {
56+
if (this._clipPath !== value) {
57+
this._clipPath = value;
58+
this.invalidateSelf();
59+
}
60+
}
61+
5162
get borderWidth(): number {
5263
return this._borderWidth;
5364
}
@@ -101,15 +112,19 @@ export module ad {
101112
let backgroundBoundsF = new android.graphics.RectF(bounds.left + backoffAntialias, bounds.top + backoffAntialias, bounds.right - backoffAntialias, bounds.bottom - backoffAntialias);
102113

103114
let outerRadius = this._cornerRadius * this._density;
104-
115+
105116
// draw background
106117
if (this.background.color && this.background.color.android) {
107118
let backgroundColorPaint = new android.graphics.Paint();
108119
backgroundColorPaint.setStyle(android.graphics.Paint.Style.FILL);
109120
backgroundColorPaint.setColor(this.background.color.android);
110121
backgroundColorPaint.setAntiAlias(true);
111122

112-
canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundColorPaint);
123+
if (this.clipPath) {
124+
drawClipPath(this.clipPath, canvas, backgroundColorPaint, backgroundBoundsF);
125+
} else {
126+
canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundColorPaint);
127+
}
113128
}
114129

115130
// draw image
@@ -139,21 +154,25 @@ export module ad {
139154
params.posX = params.repeatX ? 0 : params.posX;
140155
params.posY = params.repeatY ? 0 : params.posY;
141156

142-
let supportsPathOp = android.os.Build.VERSION.SDK_INT >= 19;
143-
if (supportsPathOp) {
144-
// Path.Op can be used in API level 19+ to achieve the perfect geometry.
145-
let backgroundPath = new android.graphics.Path();
146-
backgroundPath.addRoundRect(backgroundBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
147-
let backgroundNoRepeatPath = new android.graphics.Path();
148-
backgroundNoRepeatPath.addRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight, android.graphics.Path.Direction.CCW);
149-
(<any>backgroundPath).op(backgroundNoRepeatPath, (<any>android).graphics.Path.Op.INTERSECT);
150-
canvas.drawPath(backgroundPath, backgroundImagePaint);
157+
if (this.clipPath) {
158+
drawClipPath(this.clipPath, canvas, backgroundImagePaint, backgroundBoundsF);
151159
} else {
152-
// Clipping here will not be antialiased but at least it won't shine through the rounded corners.
153-
canvas.save();
154-
canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight);
155-
canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundImagePaint);
156-
canvas.restore();
160+
let supportsPathOp = android.os.Build.VERSION.SDK_INT >= 19;
161+
if (supportsPathOp) {
162+
// Path.Op can be used in API level 19+ to achieve the perfect geometry.
163+
let backgroundPath = new android.graphics.Path();
164+
backgroundPath.addRoundRect(backgroundBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
165+
let backgroundNoRepeatPath = new android.graphics.Path();
166+
backgroundNoRepeatPath.addRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight, android.graphics.Path.Direction.CCW);
167+
(<any>backgroundPath).op(backgroundNoRepeatPath, (<any>android).graphics.Path.Op.INTERSECT);
168+
canvas.drawPath(backgroundPath, backgroundImagePaint);
169+
} else {
170+
// Clipping here will not be antialiased but at least it won't shine through the rounded corners.
171+
canvas.save();
172+
canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight);
173+
canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundImagePaint);
174+
canvas.restore();
175+
}
157176
}
158177
}
159178

@@ -164,23 +183,29 @@ export module ad {
164183
borderPaint.setColor(this._borderColor);
165184
borderPaint.setAntiAlias(true);
166185

167-
if (outerRadius <= 0) {
168-
borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
169-
borderPaint.setStrokeWidth(borderWidth);
170-
canvas.drawRect(middleBoundsF, borderPaint);
171-
} else if (outerRadius >= borderWidth) {
186+
if (this.clipPath) {
172187
borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
173188
borderPaint.setStrokeWidth(borderWidth);
174-
let middleRadius = Math.max(0, outerRadius - halfBorderWidth);
175-
canvas.drawRoundRect(middleBoundsF, middleRadius, middleRadius, borderPaint);
189+
drawClipPath(this.clipPath, canvas, borderPaint, backgroundBoundsF);
176190
} else {
177-
let borderPath = new android.graphics.Path();
178-
let borderOuterBoundsF = new android.graphics.RectF(bounds.left, bounds.top, bounds.right, bounds.bottom);
179-
borderPath.addRoundRect(borderOuterBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
180-
let borderInnerBoundsF = new android.graphics.RectF(bounds.left + borderWidth, bounds.top + borderWidth, bounds.right - borderWidth, bounds.bottom - borderWidth);
181-
borderPath.addRect(borderInnerBoundsF, android.graphics.Path.Direction.CW);
182-
borderPaint.setStyle(android.graphics.Paint.Style.FILL);
183-
canvas.drawPath(borderPath, borderPaint);
191+
if (outerRadius <= 0) {
192+
borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
193+
borderPaint.setStrokeWidth(borderWidth);
194+
canvas.drawRect(middleBoundsF, borderPaint);
195+
} else if (outerRadius >= borderWidth) {
196+
borderPaint.setStyle(android.graphics.Paint.Style.STROKE);
197+
borderPaint.setStrokeWidth(borderWidth);
198+
let middleRadius = Math.max(0, outerRadius - halfBorderWidth);
199+
canvas.drawRoundRect(middleBoundsF, middleRadius, middleRadius, borderPaint);
200+
} else {
201+
let borderPath = new android.graphics.Path();
202+
let borderOuterBoundsF = new android.graphics.RectF(bounds.left, bounds.top, bounds.right, bounds.bottom);
203+
borderPath.addRoundRect(borderOuterBoundsF, outerRadius, outerRadius, android.graphics.Path.Direction.CCW);
204+
let borderInnerBoundsF = new android.graphics.RectF(bounds.left + borderWidth, bounds.top + borderWidth, bounds.right - borderWidth, bounds.bottom - borderWidth);
205+
borderPath.addRect(borderInnerBoundsF, android.graphics.Path.Direction.CW);
206+
borderPaint.setStyle(android.graphics.Paint.Style.FILL);
207+
canvas.drawPath(borderPath, borderPaint);
208+
}
184209
}
185210
}
186211
}
@@ -209,6 +234,8 @@ export module ad {
209234
ensureBorderDrawable();
210235
ensureLazyRequires();
211236

237+
var clipPathValue = v.style._getValue(style.clipPathProperty);
238+
212239
var backgroundValue = v.style._getValue(style.backgroundInternalProperty);
213240
var borderWidth = v.borderWidth;
214241
var bkg = <any>nativeView.getBackground();
@@ -220,7 +247,7 @@ export module ad {
220247
let backgroundColor = bkg.backgroundColor = v.style._getValue(style.backgroundColorProperty).android;
221248
bkg.setColorFilter(backgroundColor, android.graphics.PorterDuff.Mode.SRC_IN);
222249
bkg.backgroundColor = backgroundColor;
223-
} else if (v.borderWidth !== 0 || v.borderRadius !== 0 || !backgroundValue.isEmpty()) {
250+
} else if (v.borderWidth !== 0 || v.borderRadius !== 0 || !backgroundValue.isEmpty() || !clipPathValue.isEmpty()) {
224251

225252
if (!(bkg instanceof BorderDrawableClass)) {
226253
bkg = new BorderDrawableClass();
@@ -236,6 +263,7 @@ export module ad {
236263
bkg.cornerRadius = v.borderRadius;
237264
bkg.borderColor = v.borderColor ? v.borderColor.android : android.graphics.Color.TRANSPARENT;
238265
bkg.background = backgroundValue;
266+
bkg.clipPath = clipPathValue;
239267

240268
if (getSDK() < 18) {
241269
// Switch to software because of unsupported canvas methods if hardware acceleration is on:
@@ -274,3 +302,66 @@ export module ad {
274302
);
275303
}
276304
}
305+
306+
function drawClipPath(clipPath: string, canvas: android.graphics.Canvas, paint: android.graphics.Paint, bounds: android.graphics.RectF) {
307+
var functionName = clipPath.substring(0, clipPath.indexOf("("));
308+
var value = clipPath.replace(`${functionName}(`, "").replace(")", "");
309+
310+
if (functionName === "rect") {
311+
var arr = value.split(/[\s]+/);
312+
313+
var top = common.cssValueToDevicePixels(arr[0], bounds.top);
314+
var left = common.cssValueToDevicePixels(arr[1], bounds.left);
315+
var bottom = common.cssValueToDevicePixels(arr[2], bounds.bottom);
316+
var right = common.cssValueToDevicePixels(arr[3], bounds.right);
317+
318+
canvas.drawRect(left, top, right, bottom, paint);
319+
320+
} else if (functionName === "circle") {
321+
var arr = value.split(/[\s]+/);
322+
323+
var radius = common.cssValueToDevicePixels(arr[0], (bounds.width() > bounds.height() ? bounds.height() : bounds.width()) / 2);
324+
var y = common.cssValueToDevicePixels(arr[2], bounds.height());
325+
var x = common.cssValueToDevicePixels(arr[3], bounds.width());
326+
327+
canvas.drawCircle(x, y, radius, paint);
328+
329+
} else if (functionName === "ellipse") {
330+
var arr = value.split(/[\s]+/);
331+
332+
var rX = common.cssValueToDevicePixels(arr[0], bounds.right);
333+
var rY = common.cssValueToDevicePixels(arr[1], bounds.bottom);
334+
var cX = common.cssValueToDevicePixels(arr[3], bounds.right);
335+
var cY = common.cssValueToDevicePixels(arr[4], bounds.bottom);
336+
337+
var left = cX - rX;
338+
var top = cY - rY;
339+
var right = (rX * 2) + left;
340+
var bottom = (rY * 2) + top;
341+
342+
canvas.drawOval(new android.graphics.RectF(left, top, right, bottom), paint);
343+
344+
} else if (functionName === "polygon") {
345+
var path = new android.graphics.Path();
346+
var firstPoint: view.Point;
347+
var arr = value.split(/[,]+/);
348+
for (let i = 0; i < arr.length; i++) {
349+
let xy = arr[i].trim().split(/[\s]+/);
350+
let point: view.Point = {
351+
x: common.cssValueToDevicePixels(xy[0], bounds.width()),
352+
y: common.cssValueToDevicePixels(xy[1], bounds.height())
353+
};
354+
355+
if (!firstPoint) {
356+
firstPoint = point;
357+
path.moveTo(point.x, point.y);
358+
}
359+
360+
path.lineTo(point.x, point.y);
361+
}
362+
363+
path.lineTo(firstPoint.x, firstPoint.y);
364+
365+
canvas.drawPath(path, paint);
366+
}
367+
}

0 commit comments

Comments
 (0)