Non-uniform borders

This commit is contained in:
Rossen Hristov
2016-09-07 16:54:51 +03:00
parent ff30809a0a
commit a31632615d
3 changed files with 340 additions and 78 deletions

1
.gitignore vendored
View File

@@ -45,3 +45,4 @@ local.properties
.DS_Store? .DS_Store?
ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcuserdata/ ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcuserdata/
ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/ ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/
ios/TNSWidgets/DerivedData/

View File

@@ -12,6 +12,8 @@ import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.util.Log;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -20,10 +22,24 @@ import java.util.regex.Pattern;
*/ */
public class BorderDrawable extends ColorDrawable { public class BorderDrawable extends ColorDrawable {
private float density; private float density;
private float borderWidth;
private int borderColor; private int borderTopColor;
private float borderRadius; private int borderRightColor;
private int borderBottomColor;
private int borderLeftColor;
private float borderTopWidth;
private float borderRightWidth;
private float borderBottomWidth;
private float borderLeftWidth;
private float borderTopLeftRadius;
private float borderTopRightRadius;
private float borderBottomRightRadius;
private float borderBottomLeftRadius;
private String clipPath; private String clipPath;
private int backgroundColor; private int backgroundColor;
private Bitmap backgroundImage; private Bitmap backgroundImage;
private String backgroundRepeat; private String backgroundRepeat;
@@ -32,16 +48,80 @@ public class BorderDrawable extends ColorDrawable {
private String backgroundSize; private String backgroundSize;
private CSSValue[] backgroundSizeParsedCSSValues; private CSSValue[] backgroundSizeParsedCSSValues;
public float getBorderWidth() { public float getDensity() {
return borderWidth; return density;
} }
public int getBorderColor() { public int getBorderTopColor() {
return borderColor; return borderTopColor;
} }
public float getBorderRadius() { public int getBorderRightColor() {
return borderRadius; return borderRightColor;
}
public int getBorderBottomColor() {
return borderBottomColor;
}
public int getBorderLeftColor() {
return borderLeftColor;
}
public int getUniformBorderColor() {
if (this.hasUniformBorderColor()){
return this.borderTopColor;
}
return 0;
}
public float getBorderTopWidth() {
return borderTopWidth;
}
public float getBorderRightWidth() {
return borderRightWidth;
}
public float getBorderBottomWidth() {
return borderBottomWidth;
}
public float getBorderLeftWidth() {
return borderLeftWidth;
}
public float getUniformBorderWidth() {
if (this.hasUniformBorderWidth()){
return this.borderTopWidth;
}
return 0;
}
public float getBorderTopLeftRadius() {
return borderTopLeftRadius;
}
public float getBorderTopRightRadius() {
return borderTopRightRadius;
}
public float getBorderBottomRightRadius() {
return borderBottomRightRadius;
}
public float getBorderBottomLeftRadius() {
return borderBottomLeftRadius;
}
public float getUniformBorderRadius() {
if (this.hasUniformBorderRadius()){
return this.borderTopLeftRadius;
}
return 0;
} }
public String getClipPath() { public String getClipPath() {
@@ -68,11 +148,36 @@ public class BorderDrawable extends ColorDrawable {
return backgroundSize; return backgroundSize;
} }
public boolean hasUniformBorderColor() {
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;
}
public boolean hasUniformBorderRadius() {
return this.borderTopLeftRadius == this.borderTopRightRadius &&
this.borderTopLeftRadius == this.borderBottomRightRadius &&
this.borderTopLeftRadius == this.borderBottomLeftRadius;
}
public boolean hasUniformBorder(){
return this.hasUniformBorderColor() &&
this.hasUniformBorderWidth() &&
this.hasUniformBorderRadius();
}
public BorderDrawable(float density){ public BorderDrawable(float density){
super(); super();
this.density = density; this.density = density;
} }
// For backwards compatibility
public void refresh(float borderWidth, public void refresh(float borderWidth,
int borderColor, int borderColor,
float borderRadius, float borderRadius,
@@ -84,10 +189,76 @@ public class BorderDrawable extends ColorDrawable {
CSSValue[] backgroundPositionParsedCSSValues, CSSValue[] backgroundPositionParsedCSSValues,
String backgroundSize, String backgroundSize,
CSSValue[] backgroundSizeParsedCSSValues){ CSSValue[] backgroundSizeParsedCSSValues){
this.borderWidth = borderWidth; this.refresh(
this.borderColor = borderColor; borderColor,
this.borderRadius = borderRadius; borderColor,
borderColor,
borderColor,
borderWidth,
borderWidth,
borderWidth,
borderWidth,
borderRadius,
borderRadius,
borderRadius,
borderRadius,
clipPath,
backgroundColor,
backgroundImage,
backgroundRepeat,
backgroundPosition,
backgroundPositionParsedCSSValues,
backgroundSize,
backgroundSizeParsedCSSValues
);
}
public void refresh(int borderTopColor,
int borderRightColor,
int borderBottomColor,
int borderLeftColor,
float borderTopWidth,
float borderRightWidth,
float borderBottomWidth,
float borderLeftWidth,
float borderTopLeftRadius,
float borderTopRightRadius,
float borderBottomRightRadius,
float borderBottomLeftRadius,
String clipPath,
int backgroundColor,
Bitmap backgroundImage,
String backgroundRepeat,
String backgroundPosition,
CSSValue[] backgroundPositionParsedCSSValues,
String backgroundSize,
CSSValue[] backgroundSizeParsedCSSValues){
this.borderTopColor = borderTopColor;
this.borderRightColor = borderRightColor;
this.borderBottomColor = borderBottomColor;
this.borderLeftColor = borderLeftColor;
this.borderTopWidth = borderTopWidth;
this.borderRightWidth = borderRightWidth;
this.borderBottomWidth = borderBottomWidth;
this.borderLeftWidth = borderLeftWidth;
this.borderTopLeftRadius = borderTopLeftRadius;
this.borderTopRightRadius = borderTopRightRadius;
this.borderBottomRightRadius = borderBottomRightRadius;
this.borderBottomLeftRadius = borderBottomLeftRadius;
this.clipPath = clipPath; this.clipPath = clipPath;
this.backgroundColor = backgroundColor; this.backgroundColor = backgroundColor;
this.backgroundImage = backgroundImage; this.backgroundImage = backgroundImage;
this.backgroundRepeat = backgroundRepeat; this.backgroundRepeat = backgroundRepeat;
@@ -95,22 +266,26 @@ public class BorderDrawable extends ColorDrawable {
this.backgroundPositionParsedCSSValues = backgroundPositionParsedCSSValues; this.backgroundPositionParsedCSSValues = backgroundPositionParsedCSSValues;
this.backgroundSize = backgroundSize; this.backgroundSize = backgroundSize;
this.backgroundSizeParsedCSSValues = backgroundSizeParsedCSSValues; this.backgroundSizeParsedCSSValues = backgroundSizeParsedCSSValues;
this.invalidateSelf(); this.invalidateSelf();
} }
@Override @Override
public void draw(Canvas canvas) { public void draw(Canvas canvas) {
Rect bounds = this.getBounds(); Rect bounds = this.getBounds();
float borderWidth = this.borderWidth * this.density;
float halfBorderWidth = borderWidth / 2.0f;
// We will inset background colors and images so antialiasing will not color pixels outside the border. float topBackoffAntialias = calculateBackoffAntialias(this.borderTopColor, this.borderTopWidth, this.density);
// 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 rightBackoffAntialias = calculateBackoffAntialias(this.borderRightColor, this.borderRightWidth, this.density);
float normalizedBorderAlpha = ((float)Color.alpha(this.borderColor)) / 255.0f; float bottomBackoffAntialias = calculateBackoffAntialias(this.borderBottomColor, this.borderBottomWidth, this.density);
float backoffAntialias = Math.min(0.5f, halfBorderWidth) * normalizedBorderAlpha; float leftBackoffAntialias = calculateBackoffAntialias(this.borderLeftColor, this.borderLeftWidth, this.density);
RectF backgroundBoundsF = new RectF(bounds.left + backoffAntialias, bounds.top + backoffAntialias, bounds.right - backoffAntialias, bounds.bottom - backoffAntialias);
float outerRadius = this.borderRadius * this.density; RectF backgroundBoundsF = new RectF(
bounds.left + leftBackoffAntialias,
bounds.top + topBackoffAntialias,
bounds.right - rightBackoffAntialias,
bounds.bottom - bottomBackoffAntialias);
float outerRadius = this.getUniformBorderRadius() * this.density;
// draw background // draw background
if (this.backgroundColor != 0) { if (this.backgroundColor != 0) {
@@ -138,7 +313,7 @@ public class BorderDrawable extends ColorDrawable {
params.sizeX = this.backgroundImage.getWidth(); params.sizeX = this.backgroundImage.getWidth();
params.sizeY = this.backgroundImage.getHeight(); params.sizeY = this.backgroundImage.getHeight();
} }
transform.postTranslate(params.posX - backoffAntialias, params.posY - backoffAntialias); transform.postTranslate(params.posX - leftBackoffAntialias, params.posY - topBackoffAntialias);
BitmapShader shader = new BitmapShader(this.backgroundImage, android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT); BitmapShader shader = new BitmapShader(this.backgroundImage, android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT);
shader.setLocalMatrix(transform); shader.setLocalMatrix(transform);
@@ -164,7 +339,8 @@ public class BorderDrawable extends ColorDrawable {
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(backgroundPath, backgroundNoRepeatPath); intersect(backgroundPath, backgroundNoRepeatPath);
canvas.drawPath(backgroundPath, backgroundImagePaint); canvas.drawPath(backgroundPath, backgroundImagePaint);
} else { }
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.save();
canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight); canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight);
@@ -175,37 +351,142 @@ public class BorderDrawable extends ColorDrawable {
} }
// draw border // draw border
if (borderWidth > 0 && this.borderColor != 0) { if (this.hasUniformBorder()){
RectF middleBoundsF = new RectF(bounds.left + halfBorderWidth, bounds.top + halfBorderWidth, bounds.right - halfBorderWidth, bounds.bottom - halfBorderWidth); float borderWidth = this.getUniformBorderWidth() * this.density;
Paint borderPaint = new Paint(); int borderColor = this.getUniformBorderColor();
borderPaint.setColor(this.borderColor);
borderPaint.setAntiAlias(true);
if (this.clipPath != null && !this.clipPath.isEmpty()) { // iOS and browsers use black when no color is specified.
borderPaint.setStyle(Paint.Style.STROKE); if (borderColor == Color.TRANSPARENT){
borderPaint.setStrokeWidth(borderWidth); borderColor = Color.BLACK;
drawClipPath(this.clipPath, canvas, borderPaint, backgroundBoundsF, density); }
} else { if (borderWidth > 0) {
if (outerRadius <= 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()) {
borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(borderWidth); borderPaint.setStrokeWidth(borderWidth);
canvas.drawRect(middleBoundsF, borderPaint); drawClipPath(this.clipPath, canvas, borderPaint, backgroundBoundsF, density);
} else if (outerRadius >= borderWidth) { }
borderPaint.setStyle(Paint.Style.STROKE); else {
borderPaint.setStrokeWidth(borderWidth); if (outerRadius <= 0) {
float middleRadius = Math.max(0, outerRadius - halfBorderWidth); borderPaint.setStyle(Paint.Style.STROKE);
canvas.drawRoundRect(middleBoundsF, middleRadius, middleRadius, borderPaint); borderPaint.setStrokeWidth(borderWidth);
} else { canvas.drawRect(middleBoundsF, borderPaint);
Path borderPath = new Path(); }
RectF borderOuterBoundsF = new RectF(bounds.left, bounds.top, bounds.right, bounds.bottom); else if (outerRadius >= borderWidth) {
borderPath.addRoundRect(borderOuterBoundsF, outerRadius, outerRadius, Path.Direction.CCW); borderPaint.setStyle(Paint.Style.STROKE);
RectF borderInnerBoundsF = new RectF(bounds.left + borderWidth, bounds.top + borderWidth, bounds.right - borderWidth, bounds.bottom - borderWidth); borderPaint.setStrokeWidth(borderWidth);
borderPath.addRect(borderInnerBoundsF, Path.Direction.CW); float middleRadius = Math.max(0, outerRadius - halfBorderWidth);
borderPaint.setStyle(Paint.Style.FILL); canvas.drawRoundRect(middleBoundsF, middleRadius, middleRadius, borderPaint);
canvas.drawPath(borderPath, 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);
}
} }
} }
} }
else {
float top = this.borderTopWidth * this.density;
float right = this.borderRightWidth * this.density;
float bottom = this.borderBottomWidth * this.density;
float left = this.borderLeftWidth * this.density;
//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
PointF rto = new PointF(bounds.right, 0); // right-top-outside
PointF rti = new PointF(bounds.right - right, top); // right-top-outside
PointF rbo = new PointF(bounds.right, bounds.bottom); // right-bottom-outside
PointF rbi = new PointF(bounds.right - right, bounds.bottom - bottom); // right-bottom-inside
PointF lbo = new PointF(0, bounds.bottom); // left-bottom-outside
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 (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 (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 (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);
}
}
}
private static float calculateBackoffAntialias(int borderColor, float borderWidth, float density){
// 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 * density / 2.0f;
float normalizedBorderAlpha = ((float)Color.alpha(borderColor)) / 255.0f;
return Math.min(0.5f, halfBorderWidth) * normalizedBorderAlpha;
} }
@TargetApi(19) @TargetApi(19)
@@ -405,10 +686,10 @@ public class BorderDrawable extends ColorDrawable {
float result; float result;
source = source.trim(); source = source.trim();
if (source.indexOf("px") > -1) { if (source.contains("px")) {
result = Float.parseFloat(source.replace("px", "")); result = Float.parseFloat(source.replace("px", ""));
} }
else if (source.indexOf("%") > -1 && total > 0) { else if (source.contains("%") && total > 0) {
result = (Float.parseFloat(source.replace("%", "")) / 100) * (total / density); result = (Float.parseFloat(source.replace("%", "")) / 100) * (total / density);
} else { } else {
result = Float.parseFloat(source); result = Float.parseFloat(source);
@@ -424,4 +705,4 @@ public class BorderDrawable extends ColorDrawable {
private float sizeX; private float sizeX;
private float sizeY; private float sizeY;
} }
} }

View File

@@ -6,15 +6,13 @@ package org.nativescript.widgets;
import android.content.Context; import android.content.Context;
import android.graphics.*; import android.graphics.*;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.util.Log;
/** /**
* @author hhristov * @author hhristov
* *
*/ */
public class ImageView extends android.widget.ImageView { public class ImageView extends android.widget.ImageView {
private float cornerRadius = 0;
private float borderWidth = 0;
private Path path = new Path(); private Path path = new Path();
private RectF rect = new RectF(); private RectF rect = new RectF();
@@ -26,28 +24,6 @@ public class ImageView extends android.widget.ImageView {
this.setScaleType(ScaleType.FIT_CENTER); this.setScaleType(ScaleType.FIT_CENTER);
} }
public float getCornerRadius() {
return this.cornerRadius;
}
public void setCornerRadius(float radius) {
if (radius != this.cornerRadius) {
this.cornerRadius = radius;
this.invalidate();
}
}
public float getBorderWidth() {
return this.borderWidth;
}
public void setBorderWidth(float radius) {
if (radius != this.borderWidth) {
this.borderWidth = radius;
this.invalidate();
}
}
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -144,13 +120,17 @@ public class ImageView extends android.widget.ImageView {
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
BorderDrawable background = this.getBackground() instanceof BorderDrawable ? (BorderDrawable)this.getBackground() : null;
float uniformBorderWidth = background != null ? background.getUniformBorderWidth() * background.getDensity() : 0;
float uniformBorderRadius = background != null ? background.getUniformBorderRadius() * background.getDensity() : 0;
// floor the border width to avoid gaps between the border and the image // floor the border width to avoid gaps between the border and the image
float roundedBorderWidth = (float) Math.floor(this.borderWidth); float roundedBorderWidth = (float) Math.floor(uniformBorderWidth);
float innerRadius = Math.max(0, this.cornerRadius - roundedBorderWidth); float innerRadius = Math.max(0, uniformBorderRadius - roundedBorderWidth);
// The border width is included in the padding so there is no need for // The border width is included in the padding so there is no need for
// clip if there is no inner border radius. // clip if there is no inner border radius.
if (innerRadius != 0) { if (innerRadius > 0) {
this.rect.set( this.rect.set(
roundedBorderWidth, roundedBorderWidth,
roundedBorderWidth, roundedBorderWidth,