From 1ffc1628d0f9d4b506da3c7c81d97c381ca1d001 Mon Sep 17 00:00:00 2001 From: farfromrefuge Date: Wed, 11 Aug 2021 21:06:36 +0200 Subject: [PATCH] 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 --- .../core/ui/layouts/layout-base.android.ts | 12 +- .../android/widgets/build.gradle | 5 + .../nativescript/widgets/BorderDrawable.java | 619 ++++++++++++------ .../org/nativescript/widgets/LayoutBase.java | 128 +++- 4 files changed, 514 insertions(+), 250 deletions(-) diff --git a/packages/core/ui/layouts/layout-base.android.ts b/packages/core/ui/layouts/layout-base.android.ts index 9fa969fe8..7edfcd929 100644 --- a/packages/core/ui/layouts/layout-base.android.ts +++ b/packages/core/ui/layouts/layout-base.android.ts @@ -9,18 +9,8 @@ export class LayoutBase extends LayoutBaseCommon { return true; } [clipToBoundsProperty.setNative](value: boolean) { - // TODO: Use ClipRectangle if API > 16! + (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) { diff --git a/packages/ui-mobile-base/android/widgets/build.gradle b/packages/ui-mobile-base/android/widgets/build.gradle index f81817404..b01d50b23 100644 --- a/packages/ui-mobile-base/android/widgets/build.gradle +++ b/packages/ui-mobile-base/android/widgets/build.gradle @@ -51,7 +51,12 @@ def computeTargetSdkVersion() { } } + android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } compileSdkVersion computeCompileSdkVersion() buildToolsVersion computeBuildToolsVersion() diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java index 2f7853a5a..c6dda3e15 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java @@ -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 cacheKeys = new HashSet(); + + 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"); } @@ -848,4 +1041,4 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner { private float sizeX; private float sizeY; } -} \ No newline at end of file +} diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java index 7a3abe8ac..c79922717 100644 --- a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java @@ -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) { @@ -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; } -} \ No newline at end of file + + 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); + } +}