From 628e08ec02c4752195baa69db534d0eee46fbf65 Mon Sep 17 00:00:00 2001 From: Panayot Cankov Date: Wed, 19 Jul 2017 13:30:21 +0300 Subject: [PATCH] Image corners were blinking in #4322 and CSS border will now draw non uniform corner radiuses if the border color is uniform --- .gitignore | 6 + .../nativescript/widgets/BorderDrawable.java | 138 ++++++++++------- .../org/nativescript/widgets/ImageView.java | 142 +++++++++++------- build.android.sh | 2 +- build.sh | 2 +- ios/build.sh | 4 +- 6 files changed, 181 insertions(+), 113 deletions(-) diff --git a/.gitignore b/.gitignore index 111001f4a..8daa1f735 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,9 @@ local.properties ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcuserdata/ ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/ ios/TNSWidgets/DerivedData/ + +android/widgets/bin +android/widgets/.settings +android/.project +android/widgets/.project +android/.settings \ No newline at end of file diff --git a/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java b/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java index 6baad58d1..2f7c8592a 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java @@ -14,6 +14,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.Shader; import org.nativescript.widgets.image.BitmapOwner; import org.nativescript.widgets.image.Fetcher; @@ -160,6 +161,13 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner { return backgroundSize; } + public boolean hasBorderWidth() { + 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 && @@ -260,23 +268,36 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner { @Override public void draw(Canvas canvas) { Rect bounds = this.getBounds(); - if (bounds.width() <= 0 || bounds.height() <= 0) { + 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(bounds.left, bounds.top, bounds.right, bounds.bottom); + 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); - RectF backgroundBoundsF = new RectF( - bounds.left + leftBackoffAntialias, - bounds.top + topBackoffAntialias, - bounds.right - rightBackoffAntialias, - bounds.bottom - bottomBackoffAntialias); + 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 outerRadius = this.getUniformBorderRadius(); + Path backgroundPath = new Path(); + RectF backgroundRect = new RectF( + leftBackoffAntialias, + topBackoffAntialias, + width - rightBackoffAntialias, + height - bottomBackoffAntialias + ); + backgroundPath.addRoundRect(backgroundRect, backgroundRadii, Path.Direction.CW); // draw background if (this.backgroundColor != 0) { @@ -288,12 +309,12 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner { if (this.clipPath != null && !this.clipPath.isEmpty()) { drawClipPath(this.clipPath, canvas, backgroundColorPaint, backgroundBoundsF, this.density); } else { - canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundColorPaint); + canvas.drawPath(backgroundPath, backgroundColorPaint); } } if (this.backgroundBitmap != null) { - BackgroundDrawParams params = this.getDrawParams(bounds.width(), bounds.height()); + BackgroundDrawParams params = this.getDrawParams(width, height); Matrix transform = new Matrix(); if (params.sizeX > 0 && params.sizeY > 0) { float scaleX = params.sizeX / this.backgroundBitmap.getWidth(); @@ -303,16 +324,21 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner { params.sizeX = this.backgroundBitmap.getWidth(); params.sizeY = this.backgroundBitmap.getHeight(); } - transform.postTranslate(params.posX - leftBackoffAntialias, params.posY - topBackoffAntialias); - - BitmapShader shader = new BitmapShader(this.backgroundBitmap, android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT); - shader.setLocalMatrix(transform); + 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 + ); + shader.setLocalMatrix(transform); + backgroundImagePaint.setAntiAlias(true); + backgroundImagePaint.setFilterBitmap(true); backgroundImagePaint.setShader(shader); - float imageWidth = params.repeatX ? bounds.width() : params.sizeX; - float imageHeight = params.repeatY ? bounds.height() : params.sizeY; + float imageWidth = params.repeatX ? width : params.sizeX; + float imageHeight = params.repeatY ? height : params.sizeY; params.posX = params.repeatX ? 0 : params.posX; params.posY = params.repeatY ? 0 : params.posY; @@ -321,59 +347,65 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner { } else { boolean supportsPathOp = android.os.Build.VERSION.SDK_INT >= 19; if (supportsPathOp) { - // Path.Op can be used in API level 19+ to achieve the perfect geometry. - Path backgroundPath = new Path(); - backgroundPath.addRoundRect(backgroundBoundsF, outerRadius, outerRadius, Path.Direction.CCW); Path backgroundNoRepeatPath = new Path(); backgroundNoRepeatPath.addRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight, Path.Direction.CCW); - intersect(backgroundPath, backgroundNoRepeatPath); - canvas.drawPath(backgroundPath, backgroundImagePaint); + 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. canvas.save(); canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight); - canvas.drawRoundRect(backgroundBoundsF, outerRadius, outerRadius, backgroundImagePaint); + canvas.drawPath(backgroundPath, backgroundImagePaint); canvas.restore(); } } } // draw border - if (this.hasUniformBorder()) { + if (this.clipPath != null && !this.clipPath.isEmpty()) { float borderWidth = this.getUniformBorderWidth(); - int borderColor = this.getUniformBorderColor(); - - // iOS and browsers use black when no color is specified. if (borderWidth > 0) { - float halfBorderWidth = borderWidth / 2.0f; - RectF middleBoundsF = new RectF(bounds.left + halfBorderWidth, bounds.top + halfBorderWidth, bounds.right - halfBorderWidth, bounds.bottom - halfBorderWidth); Paint borderPaint = new Paint(); - borderPaint.setColor(borderColor); + borderPaint.setColor(this.getUniformBorderColor()); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(borderWidth); + drawClipPath(this.clipPath, canvas, borderPaint, backgroundBoundsF, this.density); + } + } else if (!this.hasBorderWidth()) { + // No borders trap. + } 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()); + borderPaint.setStyle(Paint.Style.FILL); borderPaint.setAntiAlias(true); - if (this.clipPath != null && !this.clipPath.isEmpty()) { - borderPaint.setStyle(Paint.Style.STROKE); - borderPaint.setStrokeWidth(borderWidth); - drawClipPath(this.clipPath, canvas, borderPaint, backgroundBoundsF, this.density); - } else { - if (outerRadius <= 0) { - borderPaint.setStyle(Paint.Style.STROKE); - borderPaint.setStrokeWidth(borderWidth); - canvas.drawRect(middleBoundsF, borderPaint); - } else if (outerRadius >= borderWidth) { - borderPaint.setStyle(Paint.Style.STROKE); - borderPaint.setStrokeWidth(borderWidth); - float middleRadius = Math.max(0, outerRadius - halfBorderWidth); - canvas.drawRoundRect(middleBoundsF, middleRadius, middleRadius, borderPaint); - } else { - Path borderPath = new Path(); - RectF borderOuterBoundsF = new RectF(bounds.left, bounds.top, bounds.right, bounds.bottom); - borderPath.addRoundRect(borderOuterBoundsF, outerRadius, outerRadius, Path.Direction.CCW); - RectF borderInnerBoundsF = new RectF(bounds.left + borderWidth, bounds.top + borderWidth, bounds.right - borderWidth, bounds.bottom - borderWidth); - borderPath.addRect(borderInnerBoundsF, Path.Direction.CW); - borderPaint.setStyle(Paint.Style.FILL); - canvas.drawPath(borderPath, borderPaint); - } - } + Path borderPath = new Path(); + + 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); } } else { float top = this.borderTopWidth; @@ -467,7 +499,7 @@ public class BorderDrawable extends ColorDrawable implements BitmapOwner { // 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(0.5f, halfBorderWidth) * normalizedBorderAlpha; + return Math.min(1f, halfBorderWidth) * normalizedBorderAlpha; } @TargetApi(19) diff --git a/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java b/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java index d76cc8f05..8344d9e32 100644 --- a/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java +++ b/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java @@ -6,11 +6,11 @@ package org.nativescript.widgets; import android.content.Context; import android.graphics.*; import android.graphics.drawable.Drawable; +import android.graphics.drawable.shapes.RoundRectShape; import org.nativescript.widgets.image.BitmapOwner; import org.nativescript.widgets.image.Fetcher; import org.nativescript.widgets.image.Worker; - /** * @author hhristov */ @@ -208,78 +208,108 @@ public class ImageView extends android.widget.ImageView implements BitmapOwner { @Override protected void onDraw(Canvas canvas) { BorderDrawable background = this.getBackground() instanceof BorderDrawable ? (BorderDrawable) this.getBackground() : null; - float uniformBorderWidth = background != null ? background.getUniformBorderWidth() : 0; - float uniformBorderRadius = background != null ? background.getUniformBorderRadius() : 0; - // floor the border width to avoid gaps between the border and the image - float roundedBorderWidth = (float) Math.floor(uniformBorderWidth); - float innerRadius = Math.max(0, uniformBorderRadius - roundedBorderWidth); + if (this.mBitmap != null) { + float borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius; - if (background != null) { - background.draw(canvas); - } + if (background != null) { + background.draw(canvas); - // The border width is included in the padding so there is no need for - // clip if there is no inner border radius. - if (innerRadius > 0) { - this.rect.set( - roundedBorderWidth, - roundedBorderWidth, - this.getWidth() - roundedBorderWidth, - this.getHeight() - roundedBorderWidth); + borderTopLeftRadius = background.getBorderTopLeftRadius(); + borderTopRightRadius = background.getBorderTopRightRadius(); + borderBottomRightRadius = background.getBorderBottomRightRadius(); + borderBottomLeftRadius = background.getBorderBottomLeftRadius(); + } else { + borderTopLeftRadius = borderTopRightRadius = borderBottomRightRadius = borderBottomLeftRadius = 0; + } - this.path.reset(); - this.path.addRoundRect(rect, innerRadius, innerRadius, Path.Direction.CW); + // Padding? + float borderTopWidth = this.getPaddingTop(); + float borderRightWidth = this.getPaddingRight(); + float borderBottomWidth = this.getPaddingBottom(); + float borderLeftWidth = this.getPaddingLeft(); - canvas.clipPath(this.path); - } + float innerWidth, innerHeight; - float rotationDegree = this.getRotationAngle(); + float rotationDegree = this.getRotationAngle(); + boolean swap = Math.abs(rotationDegree % 180) > 45 && Math.abs(rotationDegree % 180) < 135; - if (Math.abs(rotationDegree) > ImageView.EPSILON && Math.abs((rotationDegree % 90) - 0.0) < ImageView.EPSILON) { - ScaleType scaleType = this.getScaleType(); + innerWidth = this.getWidth() - borderLeftWidth - borderRightWidth; + innerHeight = this.getHeight() - borderTopWidth - borderBottomWidth; + + // TODO: Capture all created objects here in locals and update them instead... + Path path = new Path(); + float[] radii = { + 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) + }; + path.addRoundRect(new RectF(borderLeftWidth, borderTopWidth, borderLeftWidth + innerWidth, borderTopWidth + innerHeight), radii, Path.Direction.CW); + + Paint paint = new Paint(); + BitmapShader bitmapShader = new BitmapShader(this.mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - float viewWidth = this.getWidth() - (2 * roundedBorderWidth); - float viewHeight = this.getHeight() - (2 * roundedBorderWidth); float bitmapWidth = (float) mBitmap.getWidth(); float bitmapHeight = (float) mBitmap.getHeight(); - float scaleX; - float scaleY; - float decision = (rotationDegree / 90) % 2; - if (Math.abs(Math.floor(decision) - 0) < ImageView.EPSILON) { - scaleX = viewWidth / bitmapWidth; - scaleY = viewHeight / bitmapHeight; - } else { - scaleX = viewHeight / bitmapWidth; - scaleY = viewWidth / bitmapHeight; - } - - float scale = 1.0f; - - if (scaleType == ScaleType.FIT_CENTER || scaleType == ScaleType.MATRIX) { - scale = (scaleX < scaleY) ? scaleX : scaleY; - } else if (scaleType == ScaleType.CENTER_CROP) { - scale = (scaleX < scaleY) ? scaleY : scaleX; - } - Matrix matrix = this.mMatrix; matrix.reset(); - if (scaleType == ScaleType.CENTER_CROP || scaleType == ScaleType.FIT_CENTER || scaleType == ScaleType.MATRIX) { - matrix.postScale(scale, scale); - matrix.postTranslate(-(bitmapWidth * scale) / 2, -(bitmapHeight * scale) / 2); - } else if (scaleType == ScaleType.FIT_XY) { - matrix.postScale(scaleX, scaleY); - matrix.postTranslate(-((bitmapWidth * scaleX) + roundedBorderWidth) / 2, -((bitmapHeight * scaleY) + roundedBorderWidth) / 2); + matrix.postRotate(rotationDegree, bitmapWidth / 2, bitmapHeight / 2); + if (swap) { + matrix.postTranslate((bitmapHeight - bitmapWidth) / 2, (bitmapWidth - bitmapHeight) / 2); + float temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; } - matrix.postRotate(rotationDegree); - matrix.postTranslate(viewWidth / 2 + roundedBorderWidth, viewHeight / 2 + roundedBorderWidth); + float fittingScaleX = innerWidth / bitmapWidth; + float fittingScaleY = innerHeight / bitmapHeight; - canvas.drawBitmap(this.mBitmap, matrix, null); - } else { - super.onDraw(canvas); + float uniformScale; + float pivotX, pivotY; + switch(this.getScaleType()) { + case FIT_CENTER: // aspectFit + uniformScale = Math.min(fittingScaleX, fittingScaleY); + matrix.postTranslate((innerWidth - bitmapWidth) / 2, (innerHeight - bitmapHeight) / 2); + matrix.postScale(uniformScale, uniformScale, innerWidth / 2, innerHeight / 2); + canvas.clipRect( + borderLeftWidth + (innerWidth - bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight - bitmapHeight * uniformScale) / 2, + borderLeftWidth + (innerWidth + bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight + bitmapHeight * uniformScale) / 2 + ); + break; + case CENTER_CROP: // aspectFill + uniformScale = Math.max(fittingScaleX, fittingScaleY); + matrix.postTranslate((innerWidth - bitmapWidth) / 2, (innerHeight - bitmapHeight) / 2); + matrix.postScale(uniformScale, uniformScale, innerWidth / 2, innerHeight / 2); + canvas.clipRect( + borderLeftWidth + (innerWidth - bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight - bitmapHeight * uniformScale) / 2, + borderLeftWidth + (innerWidth + bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight + bitmapHeight * uniformScale) / 2 + ); + break; + case FIT_XY: // fill + matrix.postScale(fittingScaleX, fittingScaleY); + break; + case MATRIX: // none + canvas.clipRect( + borderLeftWidth, + borderTopWidth, + borderLeftWidth + bitmapWidth, + borderTopWidth + bitmapHeight + ); + break; + } + matrix.postTranslate(borderLeftWidth, borderTopWidth); + bitmapShader.setLocalMatrix(matrix); + paint.setAntiAlias(true); + paint.setFilterBitmap(true); + paint.setShader(bitmapShader); + canvas.drawPath(path, paint); } } diff --git a/build.android.sh b/build.android.sh index cda334f42..51faa6543 100755 --- a/build.android.sh +++ b/build.android.sh @@ -15,7 +15,7 @@ mkdir dist/package/platforms echo "Build android" mkdir dist/package/platforms/android cd android -./gradlew assembleRelease +./gradlew --quiet assembleRelease cd .. cp android/widgets/build/outputs/aar/widgets-release.aar dist/package/platforms/android/widgets-release.aar diff --git a/build.sh b/build.sh index f08d0e970..4079f658a 100755 --- a/build.sh +++ b/build.sh @@ -15,7 +15,7 @@ mkdir dist/package/platforms echo "Build android" mkdir dist/package/platforms/android cd android -./gradlew assembleRelease +./gradlew --quiet assembleRelease cd .. cp android/widgets/build/outputs/aar/widgets-release.aar dist/package/platforms/android/widgets-release.aar diff --git a/ios/build.sh b/ios/build.sh index 0fb32e279..e287a8e8f 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -4,10 +4,10 @@ echo "Set exit on simple errors" set -e echo "Build for iphonesimulator" -xcodebuild -project TNSWidgets/TNSWidgets.xcodeproj -sdk iphonesimulator -target TNSWidgets -configuration Release clean build CONFIGURATION_BUILD_DIR=build/Release-iphonesimulator +xcodebuild -project TNSWidgets/TNSWidgets.xcodeproj -sdk iphonesimulator -target TNSWidgets -configuration Release clean build CONFIGURATION_BUILD_DIR=build/Release-iphonesimulator -quiet echo "Build for iphoneos" -xcodebuild -project TNSWidgets/TNSWidgets.xcodeproj -sdk iphoneos -target TNSWidgets -configuration Release clean build CONFIGURATION_BUILD_DIR=build/Release-iphoneos CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO +xcodebuild -project TNSWidgets/TNSWidgets.xcodeproj -sdk iphoneos -target TNSWidgets -configuration Release clean build CONFIGURATION_BUILD_DIR=build/Release-iphoneos CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -quiet echo "Build fat framework at TNSWidgets/build/TNSWidgets.framework" rm -rf TNSWidgets/build/TNSWidgets.framework