feat(android): support clipToBounds (#9508)

* performance improvements around border handling

BREAKING CHANGE:

* if you have broder-radius or clip-path, it will clip by default
This commit is contained in:
farfromrefuge
2021-08-11 21:06:36 +02:00
committed by Nathan Walker
parent 4f5f0aae77
commit 1ffc1628d0
4 changed files with 514 additions and 250 deletions

View File

@ -9,18 +9,8 @@ export class LayoutBase extends LayoutBaseCommon {
return true;
}
[clipToBoundsProperty.setNative](value: boolean) {
// TODO: Use ClipRectangle if API > 16!
(<any>this.nativeViewProtected).setClipToBounds(value);
// 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`);
}
[isPassThroughParentEnabledProperty.setNative](value: boolean) {

View File

@ -51,7 +51,12 @@ 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;
@ -168,34 +250,33 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
}
public boolean hasBorderWidth() {
return this.borderTopWidth != 0
|| this.borderRightWidth != 0
|| this.borderBottomWidth != 0
|| this.borderLeftWidth != 0;
return this.borderTopWidth != 0 || this.borderRightWidth != 0 || this.borderBottomWidth != 0
|| this.borderLeftWidth != 0;
}
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) {
@ -209,33 +290,19 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
this.id = id;
}
public void refresh(int borderTopColor,
int borderRightColor,
int borderBottomColor,
int borderLeftColor,
public void refresh(int borderTopColor, int borderRightColor, int borderBottomColor, int borderLeftColor,
float borderTopWidth,
float borderRightWidth,
float borderBottomWidth,
float borderLeftWidth,
float borderTopWidth, float borderRightWidth, float borderBottomWidth, float borderLeftWidth,
float borderTopLeftRadius,
float borderTopRightRadius,
float borderBottomRightRadius,
float borderBottomLeftRadius,
float borderTopLeftRadius, float borderTopRightRadius, float borderBottomRightRadius,
float borderBottomLeftRadius,
String clipPath,
String clipPath,
int backgroundColor,
String backgroundImageUri,
Bitmap backgroundBitmap,
LinearGradientDefinition backgroundGradient,
Context context,
String backgroundRepeat,
String backgroundPosition,
CSSValue[] backgroundPositionParsedCSSValues,
String backgroundSize,
CSSValue[] backgroundSizeParsedCSSValues) {
int backgroundColor, String backgroundImageUri, Bitmap backgroundBitmap,
LinearGradientDefinition backgroundGradient, Context context, String backgroundRepeat,
String backgroundPosition, CSSValue[] backgroundPositionParsedCSSValues, String backgroundSize,
CSSValue[] backgroundSizeParsedCSSValues) {
this.borderTopColor = borderTopColor;
this.borderRightColor = borderRightColor;
@ -254,6 +321,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,39 +343,67 @@ 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();
private void generateBackgroundPath(Rect bounds) {
if (width <= 0 || height <= 0) {
// When the view is off-screen the bounds might be empty and we don't have anything to draw.
if (backgroundPath != null && hasCache("backgroundPath")) {
return;
}
setHasCache("backgroundPath");
RectF backgroundBoundsF = new RectF(bounds.left, bounds.top, bounds.right, bounds.bottom);
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,
width - rightBackoffAntialias,
height - bottomBackoffAntialias
);
RectF backgroundRect = new RectF(leftBackoffAntialias, topBackoffAntialias, width - rightBackoffAntialias,
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 +415,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);
}
}
@ -335,11 +434,9 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
transform.postTranslate(params.posX, params.posY);
Paint backgroundImagePaint = new Paint();
BitmapShader shader = new BitmapShader(
this.backgroundBitmap,
params.repeatX ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP,
params.repeatY ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP
);
BitmapShader shader = new BitmapShader(this.backgroundBitmap,
params.repeatX ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP,
params.repeatY ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP);
shader.setLocalMatrix(transform);
backgroundImagePaint.setAntiAlias(true);
backgroundImagePaint.setFilterBitmap(true);
@ -354,13 +451,16 @@ 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);
@ -372,10 +472,9 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
if (this.backgroundGradient != null) {
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);
LinearGradient shader = new LinearGradient(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);
@ -391,7 +490,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 +504,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;
@ -439,16 +520,16 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
float bottom = this.borderBottomWidth;
float left = this.borderLeftWidth;
//lto rto
// +---------------------+
// |lti rti|
// | |
// | |
// | |
// | |
// |lbi rbi|
// +---------------------+
//lbo rbo
// lto rto
// +---------------------+
// |lti rti|
// | |
// | |
// | |
// | |
// |lbi rbi|
// +---------------------+
// lbo rbo
PointF lto = new PointF(0, 0); // left-top-outside
PointF lti = new PointF(left, top); // left-top-inside
@ -463,66 +544,109 @@ 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 +660,22 @@ 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 +689,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 +714,21 @@ 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 +740,35 @@ 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 +804,18 @@ 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 +846,10 @@ 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())) {
@ -751,14 +897,14 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
String val = values[0].getString().toLowerCase(Locale.ENGLISH);
if ("left".equals(val) || "right".equals(val)) {
result = new CSSValue[]{values[0], center};
result = new CSSValue[] { values[0], center };
} else if ("top".equals(val) || "bottom".equals(val)) {
result = new CSSValue[]{center, values[0]};
result = new CSSValue[] { center, values[0] };
} else if ("center".equals(val)) {
result = new CSSValue[]{center, center};
result = new CSSValue[] { center, center };
}
} else if ("number".equals(values[0].getType())) {
result = new CSSValue[]{values[0], center};
result = new CSSValue[] { values[0], center };
}
}
@ -781,29 +927,22 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner {
"id: " + this.id + "; " +
"borderTopColor: " + this.borderTopColor + "; " +
"borderRightColor: " + this.borderRightColor + "; " +
"borderBottomColor: " + this.borderBottomColor + "; " +
"borderLeftColor: " + this.borderLeftColor + "; " +
"borderTopColor: " + this.borderTopColor + "; " + "borderRightColor: " + this.borderRightColor + "; "
+ "borderBottomColor: " + this.borderBottomColor + "; " + "borderLeftColor: " + this.borderLeftColor
+ "; " +
"borderTopWidth: " + this.borderTopWidth + "; " +
"borderRightWidth: " + this.borderRightWidth + "; " +
"borderBottomWidth: " + this.borderBottomWidth + "; " +
"borderLeftWidth: " + this.borderLeftWidth + "; " +
"borderTopWidth: " + this.borderTopWidth + "; " + "borderRightWidth: " + this.borderRightWidth + "; "
+ "borderBottomWidth: " + this.borderBottomWidth + "; " + "borderLeftWidth: " + this.borderLeftWidth
+ "; " +
"borderTopLeftRadius: " + this.borderTopLeftRadius + "; " +
"borderTopRightRadius: " + this.borderTopRightRadius + "; " +
"borderBottomRightRadius: " + this.borderBottomRightRadius + "; " +
"borderBottomLeftRadius: " + this.borderBottomLeftRadius + "; " +
"borderTopLeftRadius: " + this.borderTopLeftRadius + "; " + "borderTopRightRadius: "
+ this.borderTopRightRadius + "; " + "borderBottomRightRadius: " + this.borderBottomRightRadius + "; "
+ "borderBottomLeftRadius: " + this.borderBottomLeftRadius + "; " +
"clipPath: " + this.clipPath + "; " +
"backgroundColor: " + this.backgroundColor + "; " +
"backgroundImage: " + this.backgroundImage + "; " +
"backgroundBitmap: " + this.backgroundBitmap + "; " +
"backgroundRepeat: " + this.backgroundRepeat + "; " +
"backgroundPosition: " + this.backgroundPosition + "; " +
"backgroundSize: " + this.backgroundSize + "; "
;
"clipPath: " + this.clipPath + "; " + "backgroundColor: " + this.backgroundColor + "; "
+ "backgroundImage: " + this.backgroundImage + "; " + "backgroundBitmap: " + this.backgroundBitmap
+ "; " + "backgroundRepeat: " + this.backgroundRepeat + "; " + "backgroundPosition: "
+ this.backgroundPosition + "; " + "backgroundSize: " + this.backgroundSize + "; ";
}
@Override
@ -823,18 +962,72 @@ 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");
}

View File

@ -4,6 +4,14 @@
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,20 +19,46 @@ 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();
}
public LayoutBase(Context context) {
super(context);
}
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);
}
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
@ -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) {
@ -73,24 +107,25 @@ public abstract class LayoutBase extends ViewGroup {
}
// LayoutBase.onTouchEvent(ev) execution means no interactive child view handled
// the event so we let the event pass through to parent view of the layout container
// the event so we let the event pass through to parent view of the layout
// container
// 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 +134,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);
}
}