feat: clipsToBounds support

WIP - needs more work
This commit is contained in:
Igor Randjelovic
2021-09-07 22:59:14 +02:00
parent 83ea056c77
commit 95dd21f5e1
5 changed files with 524 additions and 177 deletions

View File

Binary file not shown.

View File

@@ -9,18 +9,7 @@ export class LayoutBase extends LayoutBaseCommon {
return true;
}
[clipToBoundsProperty.setNative](value: boolean) {
// TODO: Use ClipRectangle if API > 16!
// We can't implement this without calling setClipChildren(false) on every ancestor up in the visual tree,
// which will kill performance. It will also lead to unwanted side effects such as other totally unrelated
// views being affected by setting the parents' setClipChildren to false.
// The problem in Android is that a ViewGroup either clips ALL of its children or it does not. Unlike iOS, the clipping
// cannot be controlled on a per view basis. So clipToBounds=false will have to be somehow achieved with stacking different
// views on top of one another in an AbsoluteLayout or GridLayout. There is always a workaround when playing with layouts.
//
// The following article explains this in detail:
// http://stackoverflow.com/questions/25044085/when-drawing-outside-the-view-clip-bounds-with-android-how-do-i-prevent-underli
console.warn(`clipToBounds with value false is not supported on Android. You can use this.android.getParent().setClipChildren(false) as an alternative`);
(<any>this.nativeViewProtected).setClipToBounds(value);
}
[isPassThroughParentEnabledProperty.setNative](value: boolean) {

View File

@@ -52,6 +52,10 @@ def computeTargetSdkVersion() {
}
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion computeCompileSdkVersion()
buildToolsVersion computeBuildToolsVersion()

View File

@@ -18,13 +18,20 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.Shader;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.nativescript.widgets.image.BitmapOwner;
import org.nativescript.widgets.image.Fetcher;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.regex.Pattern;
import android.os.Build;
import android.util.Log;
/**
* Created by hristov on 6/15/2016.
*/
@@ -49,6 +56,20 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
private String clipPath;
private Path clipPathPath = null;
private Path backgroundPath = null;
private Path backgroundOutlinePath = null;
private Path unifiedColorBorderPath = null;
private Path innerBorderPath = null;
private Path topBorderPath = null;
private Path rightBorderPath = null;
private Path bottomBorderPath = null;
private Path leftBorderPath = null;
private Path clippingPath = null;
private Rect lastBounds = null;
Paint borderPaint = null;
private int backgroundColor;
private String backgroundImage;
private Bitmap backgroundBitmap;
@@ -141,6 +162,65 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
return clipPath;
}
public Path getClipPathPath() {
return clipPathPath;
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public Path getClippingPath() {
Path toClip = null;
if (this.clipPath != null) {
generateClipPath(this.clipPath, new RectF(getBounds()), density);
toClip = this.clipPathPath;
} else if (hasBorderWidth()) {
generateInnerBorderPath(getBounds());
toClip = this.innerBorderPath;
} else if (android.os.Build.VERSION.SDK_INT < 21 || !this.hasUniformBorder()) {
generateBackgroundOutlinePath(getBounds());
toClip = this.backgroundOutlinePath;
}
if (toClip != null) {
if (clippingPath != null && hasCache("clippingPath")) {
return clippingPath;
}
setHasCache("clippingPath");
if (clippingPath == null) {
clippingPath = new Path();
} else {
clippingPath.reset();
}
clippingPath.addRect(new RectF(lastBounds), Path.Direction.CW);
clippingPath.op(toClip, Path.Op.DIFFERENCE);
return clippingPath;
}
// if uniform borders and no border width and >= 21 then the oultine can clip
// no need to return a clippingPath
return null;
}
HashSet<String> cacheKeys = new HashSet<String>();
private boolean hasCache(String key) {
return (cacheKeys.contains(key));
}
private void setHasCache(String key) {
cacheKeys.add(key);
}
protected void onBoundsChange(Rect bounds) {
if (lastBounds != null && lastBounds.equals(bounds)) {
return;
}
if (lastBounds == null) {
lastBounds = new Rect();
}
lastBounds.set(bounds);
cacheKeys.clear();
}
public int getBackgroundColor() {
return backgroundColor;
}
@@ -153,7 +233,9 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
return backgroundBitmap;
}
public LinearGradientDefinition getBackgroundGradient() { return backgroundGradient; }
public LinearGradientDefinition getBackgroundGradient() {
return backgroundGradient;
}
public String getBackgroundRepeat() {
return backgroundRepeat;
@@ -175,27 +257,34 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
}
public boolean hasUniformBorderColor() {
return this.borderTopColor == this.borderRightColor &&
this.borderTopColor == this.borderBottomColor &&
this.borderTopColor == this.borderLeftColor;
return this.borderTopColor == this.borderRightColor
&& this.borderTopColor == this.borderBottomColor
&& this.borderTopColor == this.borderLeftColor;
}
public boolean hasUniformBorderWidth() {
return this.borderTopWidth == this.borderRightWidth &&
this.borderTopWidth == this.borderBottomWidth &&
this.borderTopWidth == this.borderLeftWidth;
return this.borderTopWidth == this.borderRightWidth
&& this.borderTopWidth == this.borderBottomWidth
&& this.borderTopWidth == this.borderLeftWidth;
}
public boolean hasUniformBorderRadius() {
return this.borderTopLeftRadius == this.borderTopRightRadius &&
this.borderTopLeftRadius == this.borderBottomRightRadius &&
this.borderTopLeftRadius == this.borderBottomLeftRadius;
return this.borderTopLeftRadius == this.borderTopRightRadius
&& this.borderTopLeftRadius == this.borderBottomRightRadius
&& this.borderTopLeftRadius == this.borderBottomLeftRadius;
}
public boolean hasBorderRadius() {
return borderBottomLeftRadius > 0
|| borderTopLeftRadius > 0
|| borderBottomRightRadius > 0
|| borderTopRightRadius > 0;
}
public boolean hasUniformBorder() {
return this.hasUniformBorderColor() &&
this.hasUniformBorderWidth() &&
this.hasUniformBorderRadius();
return this.hasUniformBorderColor()
&& this.hasUniformBorderWidth()
&& this.hasUniformBorderRadius();
}
public BorderDrawable(float density) {
@@ -254,6 +343,9 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
this.clipPath = clipPath;
// clear all cached paths
cacheKeys.clear();
this.backgroundColor = backgroundColor;
this.backgroundImage = backgroundImageUri;
this.backgroundBitmap = backgroundBitmap;
@@ -273,32 +365,33 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
}
}
@Override
public void draw(Canvas canvas) {
Rect bounds = this.getBounds();
float width = (float)bounds.width();
float height = (float)bounds.height();
if (width <= 0 || height <= 0) {
// When the view is off-screen the bounds might be empty and we don't have anything to draw.
private void generateBackgroundPath(Rect bounds) {
if (backgroundPath != null && hasCache("backgroundPath")) {
return;
}
RectF backgroundBoundsF = new RectF(bounds.left, bounds.top, bounds.right, bounds.bottom);
setHasCache("backgroundPath");
if (backgroundPath == null) {
backgroundPath = new Path();
} else {
backgroundPath.reset();
}
float[] backgroundRadii = {
Math.max(0, borderTopLeftRadius), Math.max(0, borderTopLeftRadius),
Math.max(0, borderTopRightRadius), Math.max(0, borderTopRightRadius),
Math.max(0, borderBottomRightRadius), Math.max(0, borderBottomRightRadius),
Math.max(0, borderBottomLeftRadius), Math.max(0, borderBottomLeftRadius)
};
float topBackoffAntialias = calculateBackoffAntialias(this.borderTopColor, this.borderTopWidth);
float rightBackoffAntialias = calculateBackoffAntialias(this.borderRightColor, this.borderRightWidth);
float bottomBackoffAntialias = calculateBackoffAntialias(this.borderBottomColor, this.borderBottomWidth);
float leftBackoffAntialias = calculateBackoffAntialias(this.borderLeftColor, this.borderLeftWidth);
float[] backgroundRadii = {
Math.max(0, borderTopLeftRadius + leftBackoffAntialias), Math.max(0, borderTopLeftRadius + topBackoffAntialias),
Math.max(0, borderTopRightRadius + rightBackoffAntialias), Math.max(0, borderTopRightRadius + topBackoffAntialias),
Math.max(0, borderBottomRightRadius + rightBackoffAntialias), Math.max(0, borderBottomRightRadius + bottomBackoffAntialias),
Math.max(0, borderBottomLeftRadius + leftBackoffAntialias), Math.max(0, borderBottomLeftRadius + bottomBackoffAntialias)
};
float width = (float) bounds.width();
float height = (float) bounds.height();
Path backgroundPath = new Path();
RectF backgroundRect = new RectF(
leftBackoffAntialias,
topBackoffAntialias,
@@ -306,6 +399,42 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
height - bottomBackoffAntialias
);
backgroundPath.addRoundRect(backgroundRect, backgroundRadii, Path.Direction.CW);
}
private void generateUniformedColorBorderPath(Rect bounds) {
if (unifiedColorBorderPath != null && hasCache("unifiedColorBorderPath")) {
return;
}
setHasCache("unifiedColorBorderPath");
if (unifiedColorBorderPath == null) {
unifiedColorBorderPath = new Path();
} else {
unifiedColorBorderPath.reset();
}
// the path used for outer border is the same as outline
generateBackgroundOutlinePath(bounds);
unifiedColorBorderPath.addPath(backgroundOutlinePath);
generateInnerBorderPath(bounds);
unifiedColorBorderPath.addPath(innerBorderPath);
}
@Override
public void draw(Canvas canvas) {
Rect bounds = this.getBounds();
float width = (float) bounds.width();
float height = (float) bounds.height();
if (width <= 0 || height <= 0) {
// When the view is off-screen the bounds might be empty and we don't have
// anything to draw.
return;
}
RectF backgroundBoundsF = new RectF(lastBounds);
// draw background
if (this.backgroundColor != 0) {
@@ -317,6 +446,7 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
if (this.clipPath != null && !this.clipPath.isEmpty()) {
drawClipPath(this.clipPath, canvas, backgroundColorPaint, backgroundBoundsF, this.density);
} else {
generateBackgroundPath(bounds);
canvas.drawPath(backgroundPath, backgroundColorPaint);
}
}
@@ -354,13 +484,22 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
drawClipPath(this.clipPath, canvas, backgroundImagePaint, backgroundBoundsF, this.density);
} else {
boolean supportsPathOp = android.os.Build.VERSION.SDK_INT >= 19;
generateBackgroundPath(bounds);
if (supportsPathOp) {
Path backgroundNoRepeatPath = new Path();
backgroundNoRepeatPath.addRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight, Path.Direction.CCW);
backgroundNoRepeatPath.addRect(
params.posX,
params.posY,
params.posX + imageWidth,
params.posY + imageHeight,
Path.Direction.CCW
);
intersect(backgroundNoRepeatPath, backgroundPath);
canvas.drawPath(backgroundNoRepeatPath, backgroundImagePaint);
} else {
// Clipping here will not be anti-aliased but at least it won't shine through the rounded corners.
// Clipping here will not be anti-aliased 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.drawPath(backgroundPath, backgroundImagePaint);
@@ -373,9 +512,14 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
LinearGradientDefinition def = this.backgroundGradient;
Paint backgroundGradientPaint = new Paint();
LinearGradient shader = new LinearGradient(
def.getStartX() * width, def.getStartY() * height,
def.getEndX() * width, def.getEndY() * height,
def.getColors(), def.getStops(), Shader.TileMode.MIRROR);
def.getStartX() * width,
def.getStartY() * height,
def.getEndX() * width,
def.getEndY() * height,
def.getColors(),
def.getStops(),
Shader.TileMode.MIRROR
);
backgroundGradientPaint.setAntiAlias(true);
backgroundGradientPaint.setFilterBitmap(true);
backgroundGradientPaint.setShader(shader);
@@ -383,6 +527,7 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
if (this.clipPath != null && !this.clipPath.isEmpty()) {
drawClipPath(this.clipPath, canvas, backgroundGradientPaint, backgroundBoundsF, this.density);
} else {
generateBackgroundPath(bounds);
canvas.drawPath(backgroundPath, backgroundGradientPaint);
}
}
@@ -391,7 +536,10 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
if (this.clipPath != null && !this.clipPath.isEmpty()) {
float borderWidth = this.getUniformBorderWidth();
if (borderWidth > 0) {
Paint borderPaint = new Paint();
if (borderPaint == null) {
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
}
borderPaint.setColor(this.getUniformBorderColor());
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(borderWidth);
@@ -402,36 +550,15 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
} else if (this.hasUniformBorderColor()) {
// iOS and browsers use black when no color is specified.
if (borderLeftWidth > 0 || borderTopWidth > 0 || borderRightWidth > 0 || borderBottomWidth > 0) {
Paint borderPaint = new Paint();
borderPaint.setColor(this.getUniformBorderColor());
if (borderPaint == null) {
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
}
borderPaint.setStyle(Paint.Style.FILL);
borderPaint.setAntiAlias(true);
Path borderPath = new Path();
borderPaint.setColor(this.getUniformBorderColor());
RectF borderOuterRect = new RectF(0, 0, width, height);
float[] borderOuterRadii = {
borderTopLeftRadius, borderTopLeftRadius,
borderTopRightRadius, borderTopRightRadius,
borderBottomRightRadius, borderBottomRightRadius,
borderBottomLeftRadius, borderBottomLeftRadius
};
borderPath.addRoundRect(borderOuterRect, borderOuterRadii, Path.Direction.CW);
RectF borderInnerRect = new RectF(
borderLeftWidth,
borderTopWidth,
width - borderRightWidth,
height - borderBottomWidth
);
float[] borderInnerRadii = {
Math.max(0, borderTopLeftRadius - borderLeftWidth), Math.max(0, borderTopLeftRadius - borderTopWidth),
Math.max(0, borderTopRightRadius - borderRightWidth), Math.max(0, borderTopRightRadius - borderTopWidth),
Math.max(0, borderBottomRightRadius - borderRightWidth), Math.max(0, borderBottomRightRadius - borderBottomWidth),
Math.max(0, borderBottomLeftRadius - borderLeftWidth), Math.max(0, borderBottomLeftRadius - borderBottomWidth)
};
borderPath.addRoundRect(borderInnerRect, borderInnerRadii, Path.Direction.CCW);
canvas.drawPath(borderPath, borderPaint);
generateUniformedColorBorderPath(bounds);
canvas.drawPath(unifiedColorBorderPath, borderPaint);
}
} else {
float top = this.borderTopWidth;
@@ -463,66 +590,114 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
PointF lbi = new PointF(left, bounds.bottom - bottom); // left-bottom-inside
if (this.borderTopWidth > 0) {
Paint topBorderPaint = new Paint();
topBorderPaint.setColor(this.borderTopColor);
topBorderPaint.setAntiAlias(true);
Path topBorderPath = new Path();
topBorderPath.setFillType(Path.FillType.EVEN_ODD);
topBorderPath.moveTo(lto.x, lto.y);
topBorderPath.lineTo(rto.x, rto.y);
topBorderPath.lineTo(rti.x, rti.y);
topBorderPath.lineTo(lti.x, lti.y);
topBorderPath.close();
canvas.drawPath(topBorderPath, topBorderPaint);
if (topBorderPath == null || !hasCache("topBorderPath")) {
setHasCache("topBorderPath");
if (topBorderPath == null) {
topBorderPath = new Path();
} else {
topBorderPath.reset();
}
topBorderPath.setFillType(Path.FillType.EVEN_ODD);
topBorderPath.moveTo(lto.x, lto.y);
topBorderPath.lineTo(rto.x, rto.y);
topBorderPath.lineTo(rti.x, rti.y);
topBorderPath.lineTo(lti.x, lti.y);
topBorderPath.close();
}
if (borderPaint == null) {
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
}
borderPaint.setColor(this.borderTopColor);
canvas.drawPath(topBorderPath, borderPaint);
}
if (this.borderRightWidth > 0) {
Paint rightBorderPaint = new Paint();
rightBorderPaint.setColor(this.borderRightColor);
rightBorderPaint.setAntiAlias(true);
Path rightBorderPath = new Path();
rightBorderPath.setFillType(Path.FillType.EVEN_ODD);
rightBorderPath.moveTo(rto.x, rto.y);
rightBorderPath.lineTo(rbo.x, rbo.y);
rightBorderPath.lineTo(rbi.x, rbi.y);
rightBorderPath.lineTo(rti.x, rti.y);
rightBorderPath.close();
canvas.drawPath(rightBorderPath, rightBorderPaint);
if (rightBorderPath == null || !hasCache("rightBorderPath")) {
setHasCache("rightBorderPath");
if (rightBorderPath == null) {
rightBorderPath = new Path();
} else {
rightBorderPath.reset();
}
rightBorderPath.setFillType(Path.FillType.EVEN_ODD);
rightBorderPath.moveTo(rto.x, rto.y);
rightBorderPath.lineTo(rbo.x, rbo.y);
rightBorderPath.lineTo(rbi.x, rbi.y);
rightBorderPath.lineTo(rti.x, rti.y);
rightBorderPath.close();
}
if (borderPaint == null) {
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
}
borderPaint.setColor(this.borderRightColor);
canvas.drawPath(rightBorderPath, borderPaint);
}
if (this.borderBottomWidth > 0) {
Paint bottomBorderPaint = new Paint();
bottomBorderPaint.setColor(this.borderBottomColor);
bottomBorderPaint.setAntiAlias(true);
Path bottomBorderPath = new Path();
bottomBorderPath.setFillType(Path.FillType.EVEN_ODD);
bottomBorderPath.moveTo(rbo.x, rbo.y);
bottomBorderPath.lineTo(lbo.x, lbo.y);
bottomBorderPath.lineTo(lbi.x, lbi.y);
bottomBorderPath.lineTo(rbi.x, rbi.y);
bottomBorderPath.close();
canvas.drawPath(bottomBorderPath, bottomBorderPaint);
if (bottomBorderPath == null || !hasCache("bottomBorderPath")) {
setHasCache("bottomBorderPath");
if (bottomBorderPath == null) {
bottomBorderPath = new Path();
} else {
bottomBorderPath.reset();
}
bottomBorderPath.setFillType(Path.FillType.EVEN_ODD);
bottomBorderPath.moveTo(rbo.x, rbo.y);
bottomBorderPath.lineTo(lbo.x, lbo.y);
bottomBorderPath.lineTo(lbi.x, lbi.y);
bottomBorderPath.lineTo(rbi.x, rbi.y);
bottomBorderPath.close();
}
if (borderPaint == null) {
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
}
borderPaint.setColor(this.borderBottomColor);
canvas.drawPath(bottomBorderPath, borderPaint);
}
if (this.borderLeftWidth > 0) {
Paint leftBorderPaint = new Paint();
leftBorderPaint.setColor(this.borderLeftColor);
leftBorderPaint.setAntiAlias(true);
Path leftBorderPath = new Path();
leftBorderPath.setFillType(Path.FillType.EVEN_ODD);
leftBorderPath.moveTo(lbo.x, lbo.y);
leftBorderPath.lineTo(lto.x, lto.y);
leftBorderPath.lineTo(lti.x, lti.y);
leftBorderPath.lineTo(lbi.x, lbi.y);
leftBorderPath.close();
canvas.drawPath(leftBorderPath, leftBorderPaint);
if (leftBorderPath == null || !hasCache("leftBorderPath")) {
setHasCache("leftBorderPath");
if (leftBorderPath == null) {
leftBorderPath = new Path();
} else {
leftBorderPath.reset();
}
leftBorderPath.setFillType(Path.FillType.EVEN_ODD);
leftBorderPath.moveTo(lbo.x, lbo.y);
leftBorderPath.lineTo(lto.x, lto.y);
leftBorderPath.lineTo(lti.x, lti.y);
leftBorderPath.lineTo(lbi.x, lbi.y);
leftBorderPath.close();
}
if (borderPaint == null) {
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
}
borderPaint.setColor(this.borderLeftColor);
canvas.drawPath(leftBorderPath, borderPaint);
}
}
}
private static float calculateBackoffAntialias(int borderColor, float borderWidth) {
// We will inset background colors and images so antialiasing will not color pixels outside the border.
// If the border is transparent we will backoff less, and we will not backoff more than half a pixel or half the border width.
// We will inset background colors and images so antialiasing will not color
// pixels outside the border.
// If the border is transparent we will backoff less, and we will not backoff
// more than half a pixel or half the border width.
float halfBorderWidth = borderWidth / 2.0f;
float normalizedBorderAlpha = ((float) Color.alpha(borderColor)) / 255.0f;
return Math.min(1f, halfBorderWidth) * normalizedBorderAlpha;
@@ -536,11 +711,23 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
private static Pattern spaceAndComma = Pattern.compile("[\\s,]+");
private static Pattern space = Pattern.compile("\\s+");
private static void drawClipPath(String clipPath, Canvas canvas, Paint paint, RectF bounds, float density) {
// Sample string is 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%);
private void generateClipPath(String clipPath, RectF bounds, float density) {
if (clipPathPath != null && hasCache("clipPathPath")) {
return;
}
setHasCache("clipPathPath");
// Sample string is 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%);
String functionName = clipPath.substring(0, clipPath.indexOf("("));
String value = clipPath.substring(clipPath.indexOf("(") + 1, clipPath.indexOf(")"));
if (clipPathPath == null) {
clipPathPath = new Path();
} else {
clipPathPath.reset();
}
String[] arr;
float top;
float right;
@@ -554,8 +741,7 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
right = cssValueToDevicePixels(arr[1], bounds.right, density);
bottom = cssValueToDevicePixels(arr[2], bounds.bottom, density);
left = cssValueToDevicePixels(arr[3], bounds.right, density);
canvas.drawRect(left, top, right, bottom, paint);
clipPathPath.addRect(new RectF(left, top, right, bottom), Path.Direction.CW);
break;
case "inset":
arr = spaceAndComma.split(value);
@@ -580,18 +766,23 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
}
top = cssValueToDevicePixels(topString, bounds.bottom, density);
right = cssValueToDevicePixels("100%", bounds.right, density) - cssValueToDevicePixels(rightString, bounds.right, density);
bottom = cssValueToDevicePixels("100%", bounds.bottom, density) - cssValueToDevicePixels(bottomString, bounds.bottom, density);
right = cssValueToDevicePixels("100%", bounds.right, density)
- cssValueToDevicePixels(rightString, bounds.right, density);
bottom = cssValueToDevicePixels("100%", bounds.bottom, density)
- cssValueToDevicePixels(bottomString, bounds.bottom, density);
left = cssValueToDevicePixels(leftString, bounds.right, density);
canvas.drawRect(left, top, right, bottom, paint);
clipPathPath.addRect(new RectF(left, top, right, bottom), Path.Direction.CW);
break;
case "circle":
arr = space.split(value);
float radius = cssValueToDevicePixels(arr[0], (bounds.width() > bounds.height() ? bounds.height() : bounds.width()) / 2, density);
float radius = cssValueToDevicePixels(arr[0],
(bounds.width() > bounds.height() ? bounds.height() : bounds.width()) / 2,
density
);
float y = cssValueToDevicePixels(arr[2], bounds.height(), density);
float x = cssValueToDevicePixels(arr[3], bounds.width(), density);
canvas.drawCircle(x, y, radius, paint);
clipPathPath.addCircle(x, y, radius, Path.Direction.CW);
break;
case "ellipse":
arr = space.split(value);
@@ -603,31 +794,37 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
top = cY - rY;
right = (rX * 2) + left;
bottom = (rY * 2) + top;
canvas.drawOval(new RectF(left, top, right, bottom), paint);
clipPathPath.addOval(new RectF(left, top, right, bottom), Path.Direction.CW);
break;
case "polygon":
Path path = new Path();
PointF firstPoint = null;
arr = value.split(",");
for (String s : arr) {
String[] xy = space.split(s.trim());
PointF point = new PointF(cssValueToDevicePixels(xy[0], bounds.width(), density), cssValueToDevicePixels(xy[1], bounds.height(), density));
PointF point = new PointF(
cssValueToDevicePixels(xy[0], bounds.width(), density),
cssValueToDevicePixels(xy[1], bounds.height(), density)
);
if (firstPoint == null) {
firstPoint = point;
path.moveTo(point.x, point.y);
clipPathPath.moveTo(point.x, point.y);
}
path.lineTo(point.x, point.y);
clipPathPath.lineTo(point.x, point.y);
}
if (firstPoint != null) {
path.lineTo(firstPoint.x, firstPoint.y);
clipPathPath.lineTo(firstPoint.x, firstPoint.y);
}
canvas.drawPath(path, paint);
break;
}
}
private void drawClipPath(String clipPath, Canvas canvas, Paint paint, RectF bounds, float density) {
generateClipPath(clipPath, bounds, density);
canvas.drawPath(clipPathPath, paint);
}
private BackgroundDrawParams getDrawParams(float width, float height) {
BackgroundDrawParams res = new BackgroundDrawParams();
@@ -663,15 +860,26 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
res.sizeX = imageWidth;
res.sizeY = imageHeight;
} else if ("number".equals(vx.getType()) && "number".equals(vy.getType()) &&
(("px".equals(vx.getUnit()) && "px".equals(vy.getUnit())) || ((vx.getUnit() == null || vx.getUnit().isEmpty()) && (vy.getUnit() == null || vy.getUnit().isEmpty())))) {
} else if ("number".equals(vx.getType())
&& "number".equals(vy.getType())
&& (
("px".equals(vx.getUnit()) && "px".equals(vy.getUnit()))
|| (
(vx.getUnit() == null || vx.getUnit().isEmpty())
&& (vy.getUnit() == null || vy.getUnit().isEmpty())
)
)
) {
imageWidth = vx.getValue();
imageHeight = vy.getValue();
res.sizeX = imageWidth;
res.sizeY = imageHeight;
}
} else if (this.backgroundSizeParsedCSSValues.length == 1 && "ident".equals(this.backgroundSizeParsedCSSValues[0].getType())) {
} else if (
this.backgroundSizeParsedCSSValues.length == 1
&& "ident".equals(this.backgroundSizeParsedCSSValues[0].getType())
) {
float scale = 0;
if ("cover".equals(this.backgroundSizeParsedCSSValues[0].getString())) {
@@ -702,8 +910,17 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
if ("%".equals(vx.getUnit()) && "%".equals(vy.getUnit())) {
res.posX = spaceX * vx.getValue() / 100;
res.posY = spaceY * vy.getValue() / 100;
} else if ("number".equals(vx.getType()) && "number".equals(vy.getType()) &&
(("px".equals(vx.getUnit()) && "px".equals(vy.getUnit())) || ((vx.getUnit() == null || vx.getUnit().isEmpty()) && (vy.getUnit() == null || vy.getUnit().isEmpty())))) {
} else if (
"number".equals(vx.getType())
&& "number".equals(vy.getType())
&& (
("px".equals(vx.getUnit()) && "px".equals(vy.getUnit()))
|| (
(vx.getUnit() == null || vx.getUnit().isEmpty())
&& (vy.getUnit() == null || vy.getUnit().isEmpty())
)
)
) {
res.posX = vx.getValue();
res.posY = vy.getValue();
} else if ("ident".equals(vx.getType()) && "ident".equals(vy.getType())) {
@@ -823,18 +1040,80 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
return drawable;
}
private void generateBackgroundOutlinePath(Rect bounds) {
if (backgroundOutlinePath != null && hasCache("backgroundOutlinePath")) {
return;
}
setHasCache("backgroundOutlinePath");
if (backgroundOutlinePath == null) {
backgroundOutlinePath = new Path();
} else {
backgroundOutlinePath.reset();
}
float[] backgroundRadii = {
Math.max(0, borderTopLeftRadius), Math.max(0, borderTopLeftRadius),
Math.max(0, borderTopRightRadius), Math.max(0, borderTopRightRadius),
Math.max(0, borderBottomRightRadius), Math.max(0, borderBottomRightRadius),
Math.max(0, borderBottomLeftRadius), Math.max(0, borderBottomLeftRadius)
};
backgroundOutlinePath.addRoundRect(new RectF(bounds), backgroundRadii, Path.Direction.CW);
}
private void generateInnerBorderPath(Rect bounds) {
if (innerBorderPath != null && hasCache("innerBorderPath")) {
return;
}
setHasCache("innerBorderPath");
if (innerBorderPath == null) {
innerBorderPath = new Path();
} else {
innerBorderPath.reset();
}
float width = (float) bounds.width();
float height = (float) bounds.height();
RectF borderInnerRect = new RectF(borderLeftWidth, borderTopWidth, width - borderRightWidth,
height - borderBottomWidth);
float[] borderInnerRadii = {
Math.max(0, borderTopLeftRadius - borderLeftWidth),
Math.max(0, borderTopLeftRadius - borderTopWidth),
Math.max(0, borderTopRightRadius - borderRightWidth),
Math.max(0, borderTopRightRadius - borderTopWidth),
Math.max(0, borderBottomRightRadius - borderRightWidth),
Math.max(0, borderBottomRightRadius - borderBottomWidth),
Math.max(0, borderBottomLeftRadius - borderLeftWidth),
Math.max(0, borderBottomLeftRadius - borderBottomWidth)
};
innerBorderPath.addRoundRect(borderInnerRect, borderInnerRadii, Path.Direction.CCW);
}
public boolean shouldOutline() {
return (android.os.Build.VERSION.SDK_INT >= 21 && getUniformBorderRadius() > 0 && !hasBorderWidth());
}
@Override
public void getOutline(@NonNull Outline outline) {
if (android.os.Build.VERSION.SDK_INT >= 21) {
Path backgroundPath = new Path();
float[] backgroundRadii = {
Math.max(0, borderTopLeftRadius), Math.max(0, borderTopLeftRadius),
Math.max(0, borderTopRightRadius), Math.max(0, borderTopRightRadius),
Math.max(0, borderBottomRightRadius), Math.max(0, borderBottomRightRadius),
Math.max(0, borderBottomLeftRadius), Math.max(0, borderBottomLeftRadius)
};
backgroundPath.addRoundRect(new RectF(getBounds()), backgroundRadii, Path.Direction.CW);
outline.setConvexPath(backgroundPath);
if (this.clipPath != null) {
// no clip!
generateClipPath(this.clipPath, new RectF(getBounds()), density);
outline.setConvexPath(this.clipPathPath);
} else if (hasUniformBorder()) {
// clip!
outline.setRoundRect(getBounds(), Math.max(0, borderTopLeftRadius));
} else {
// no clip!
generateBackgroundOutlinePath(getBounds());
if (backgroundOutlinePath != null) {
outline.setConvexPath(backgroundOutlinePath);
}
}
} else {
throw new IllegalStateException("Method supported on API 21 or higher");
}
@@ -848,4 +1127,4 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
private float sizeX;
private float sizeY;
}
}
}

View File

@@ -1,9 +1,17 @@
/**
*
*
*/
package org.nativescript.widgets;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
@@ -11,32 +19,58 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.util.Log;
import androidx.annotation.RequiresApi;
/**
* @author hhristov
*
*/
public abstract class LayoutBase extends ViewGroup {
private boolean passThroughParent;
private boolean clipEnabled = true;
private static Paint clipPaint;
public LayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setLayerType(View.LAYER_TYPE_HARDWARE, null);
setClipToBounds(clipEnabled);
}
public LayoutBase(Context context) {
super(context);
init();
}
public void setClipToBounds(boolean value) {
clipEnabled = value;
if (value) {
// TODO: does it cost to enable it even if we actually
// will still need to clip?
if (android.os.Build.VERSION.SDK_INT >= 21) {
setClipToOutline(true);
}
} else if (android.os.Build.VERSION.SDK_INT >= 21) {
setClipToOutline(false);
}
}
public LayoutBase(Context context) {
super(context);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new CommonLayoutParams();
}
/**
* {@inheritDoc}
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CommonLayoutParams();
return new CommonLayoutParams();
}
/**
@@ -50,21 +84,21 @@ public abstract class LayoutBase extends ViewGroup {
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) {
if (from instanceof CommonLayoutParams)
return new CommonLayoutParams((CommonLayoutParams)from);
return new CommonLayoutParams((CommonLayoutParams) from);
if (from instanceof FrameLayout.LayoutParams)
return new CommonLayoutParams((FrameLayout.LayoutParams)from);
return new CommonLayoutParams((FrameLayout.LayoutParams) from);
if (from instanceof ViewGroup.MarginLayoutParams)
return new CommonLayoutParams((ViewGroup.MarginLayoutParams)from);
return new CommonLayoutParams((ViewGroup.MarginLayoutParams) from);
return new CommonLayoutParams(from);
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
@Override
public boolean shouldDelayChildPressedState() {
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
@@ -77,20 +111,20 @@ public abstract class LayoutBase extends ViewGroup {
// because passThroughParent is set to true
return false;
}
protected static int getGravity(View view) {
int gravity = -1;
LayoutParams params = view.getLayoutParams();
if (params instanceof FrameLayout.LayoutParams) {
gravity = ((FrameLayout.LayoutParams)params).gravity;
}
protected static int getGravity(View view) {
int gravity = -1;
LayoutParams params = view.getLayoutParams();
if (params instanceof FrameLayout.LayoutParams) {
gravity = ((FrameLayout.LayoutParams) params).gravity;
}
if (gravity == -1) {
gravity = Gravity.FILL;
}
return gravity;
}
}
public boolean getPassThroughParent() {
return this.passThroughParent;
@@ -99,4 +133,45 @@ public abstract class LayoutBase extends ViewGroup {
public void setPassThroughParent(boolean value) {
this.passThroughParent = value;
}
}
public boolean getClipEnabled() {
return this.clipEnabled;
}
public void setClipEnabled(boolean value) {
this.clipEnabled = value;
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
protected void dispatchDraw(Canvas canvas) {
if (clipEnabled) {
Drawable drawable = getBackground();
if (drawable instanceof BorderDrawable) {
Path clippingPath = ((BorderDrawable) drawable).getClippingPath();
// if no clippingPath either it is unnecessary or handled by outline
if (clippingPath != null) {
if (LayoutBase.clipPaint == null) {
LayoutBase.clipPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
// LayoutBase.clipPaint.setColor(Color.WHITE);
LayoutBase.clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
int saveCount;
int width = getWidth();
int height = getHeight();
if (android.os.Build.VERSION.SDK_INT >= 21) {
saveCount = canvas.saveLayer(new android.graphics.RectF(0.0f, 0.0f, width, height), null);
} else {
saveCount = canvas.saveLayer(0.0f, 0.0f, width, height, null, Canvas.ALL_SAVE_FLAG);
}
super.dispatchDraw(canvas);
// we dont use clipPath as it is not antialiased
canvas.drawPath(clippingPath, LayoutBase.clipPaint);
canvas.restoreToCount(saveCount);
return;
}
}
}
super.dispatchDraw(canvas);
}
}