Merge pull request #103 from NativeScript/image-and-corners

Implement non uniform border corner radiuses and fix blinking image-view radiuses
This commit is contained in:
Panayot Cankov
2017-07-27 13:49:04 +03:00
committed by GitHub
6 changed files with 181 additions and 113 deletions

6
.gitignore vendored
View File

@@ -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

View File

@@ -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,60 +347,66 @@ 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()) {
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.setAntiAlias(true);
if (this.clipPath != null && !this.clipPath.isEmpty()) {
float borderWidth = this.getUniformBorderWidth();
if (borderWidth > 0) {
Paint borderPaint = new Paint();
borderPaint.setColor(this.getUniformBorderColor());
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);
}
} 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);
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;
float right = this.borderRightWidth;
@@ -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)

View File

@@ -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);
borderTopLeftRadius = background.getBorderTopLeftRadius();
borderTopRightRadius = background.getBorderTopRightRadius();
borderBottomRightRadius = background.getBorderBottomRightRadius();
borderBottomLeftRadius = background.getBorderBottomLeftRadius();
} else {
borderTopLeftRadius = borderTopRightRadius = borderBottomRightRadius = borderBottomLeftRadius = 0;
}
// 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);
// Padding?
float borderTopWidth = this.getPaddingTop();
float borderRightWidth = this.getPaddingRight();
float borderBottomWidth = this.getPaddingBottom();
float borderLeftWidth = this.getPaddingLeft();
this.path.reset();
this.path.addRoundRect(rect, innerRadius, innerRadius, Path.Direction.CW);
canvas.clipPath(this.path);
}
float innerWidth, innerHeight;
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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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