/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.ui.plaf;
import com.codename1.ui.Component;
import com.codename1.ui.Display;
import com.codename1.ui.Graphics;
import com.codename1.ui.Image;
import com.codename1.ui.Stroke;
import com.codename1.ui.geom.GeneralPath;
import com.codename1.ui.geom.Rectangle;
/**
* <p>A border that can either be a circle or a circular rectangle which is a rectangle whose sides are circles.
* This border can optionally have a drop shadow associated with it.</p>
* <script src="https://gist.github.com/codenameone/3e91e5eab4e677e6b03962e78ae99e07.js"></script>
* <img src="https://www.codenameone.com/img/blog/round-border.png" alt="Round Border" />
*
*
* @author Shai Almog
*/
public class RoundBorder extends Border {
private static final String CACHE_KEY = "cn1$$-rbcache";
/**
* The color of the border background
*/
private int color = 0xd32f2f;
/**
* The opacity (transparency) of the border background
*/
private int opacity = 255;
/**
* The color of the edge of the border if applicable
*/
private int strokeColor;
/**
* The opacity of the edge of the border if applicable
*/
private int strokeOpacity = 255;
private Stroke stroke;
/**
* The thickness of the edge of the border if applicable, 0 if no stroke is needed
*/
private float strokeThickness;
/**
* True if the thickness of the stroke is in millimeters
*/
private boolean strokeMM;
/**
* The spread of the shadow in pixels of millimeters
*/
private int shadowSpread;
/**
* The opacity of the shadow between 0 and 255
*/
private int shadowOpacity = 0;
/**
* X axis bias of the shadow between 0 and 1 where 0 is to the top and 1 is to the bottom, defaults to 0.5
*/
private float shadowX = 0.5f;
/**
* Y axis bias of the shadow between 0 and 1 where 0 is to the left and 1 is to the right, defaults to 0.5
*/
private float shadowY = 0.5f;
/**
* The Gaussian blur size
*/
private float shadowBlur = 10;
/**
* True if the shadow spread is in millimeters
*/
private boolean shadowMM;
/**
* True if this border grows into a rectangle horizontally or keeps growing as a circle
*/
private boolean rectangle;
// these allow us to have more than one border per component in cache which is important for selected/unselected/pressed values
private static int instanceCounter;
private final int instanceVal;
private boolean uiid;
private RoundBorder() {
shadowSpread = Display.getInstance().convertToPixels(2);
instanceCounter++;
instanceVal = instanceCounter;
}
/**
* Creates a flat round border with no stroke and no shadow and the default color, this call can
* be chained with the other calls to mutate the color/opacity etc.
* @return a border instance
*/
public static RoundBorder create() {
return new RoundBorder();
}
/**
* <p>Uses the style of the components UIID to draw the background of the border, this effectively overrides all
* other style settings but allows the full power of UIID drawing including gradients, background images
* etc.</p>
* <p><strong>Notice: </strong>this flag will only work when shaped clipping is supported. That feature
* isn't available in all platforms...</p>
*
*
* @param uiid true to use the background of the component setting
* @return border instance so these calls can be chained
*/
public RoundBorder uiid(boolean uiid) {
this.uiid = uiid;
return this;
}
/**
* True is we use the background of the component setting to draw
* @return true if we draw based on the component UIID
*/
public boolean getUIID() {
return uiid;
}
/**
* Sets the background color of the circle/rectangle
* @param color the color
* @return border instance so these calls can be chained
*/
public RoundBorder color(int color) {
this.color = color;
return this;
}
/**
* Sets the background opacity of the circle/rectangle
* @param opacity the background opacity from 0-255 where 255 is completely opaque
* @return border instance so these calls can be chained
*/
public RoundBorder opacity(int opacity) {
this.opacity = opacity;
return this;
}
/**
* Sets the opacity of the stroke line around the circle/rectangle
* @param strokeOpacity the opacity from 0-255 where 255 is completely opaque
* @return border instance so these calls can be chained
*/
public RoundBorder strokeOpacity(int strokeOpacity) {
this.strokeOpacity = strokeOpacity;
return this;
}
/**
* Sets the stroke color of the circle/rectangle
* @param strokeColor the color
* @return border instance so these calls can be chained
*/
public RoundBorder strokeColor(int strokeColor) {
this.strokeColor = strokeColor;
return this;
}
/**
* Sets the stroke of the circle/rectangle
* @param stroke the stroke object
* @return border instance so these calls can be chained
*/
public RoundBorder stroke(Stroke stroke) {
this.stroke = stroke;
return this;
}
/**
* Sets the stroke of the circle/rectangle
* @param stroke the thickness of the stroke object
* @param mm set to true to indicate the value is in millimeters, false indicates pixels
* @return border instance so these calls can be chained
*/
public RoundBorder stroke(float stroke, boolean mm) {
strokeThickness = stroke;
if(strokeThickness == 0) {
this.stroke = null;
return this;
}
strokeMM = mm;
if(mm) {
stroke = Display.getInstance().convertToPixels(stroke);
}
return stroke(new Stroke(stroke, Stroke.CAP_SQUARE, Stroke.JOIN_MITER, 1));
}
/**
* Sets the spread in pixels of the shadow i.e how much bigger is it than the actual circle/rectangle
* @param shadowSpread the amount in pixels representing the size of the shadow
* @param mm set to true to indicate the value is in millimeters, false indicates pixels
* @return border instance so these calls can be chained
*/
public RoundBorder shadowSpread(int shadowSpread, boolean mm) {
this.shadowMM = mm;
this.shadowSpread = shadowSpread;
return this;
}
/**
* Sets the spread in pixels of the shadow i.e how much bigger is it than the actual circle/rectangle
* @param shadowSpread the amount in pixels representing the size of the shadow
* @return border instance so these calls can be chained
*/
public RoundBorder shadowSpread(int shadowSpread) {
this.shadowSpread = shadowSpread;
return this;
}
/**
* Sets the opacity of the shadow from 0 - 255 where 0 means no shadow and 255 means opaque black shadow
* @param shadowOpacity the opacity of the shadow
* @return border instance so these calls can be chained
*/
public RoundBorder shadowOpacity(int shadowOpacity) {
this.shadowOpacity = shadowOpacity;
return this;
}
/**
* The position of the shadow on the X axis where 0.5f means the center and higher values draw it to the right side
* @param shadowX the position of the shadow between 0 - 1 where 0 equals left and 1 equals right
* @return border instance so these calls can be chained
*/
public RoundBorder shadowX(float shadowX) {
this.shadowX = shadowX;
return this;
}
/**
* The position of the shadow on the Y axis where 0.5f means the center and higher values draw it to the bottom
* @param shadowY the position of the shadow between 0 - 1 where 0 equals top and 1 equals bottom
* @return border instance so these calls can be chained
*/
public RoundBorder shadowY(float shadowY) {
this.shadowY = shadowY;
return this;
}
/**
* The blur on the shadow this is the standard Gaussian blur radius
* @param shadowBlur The blur on the shadow this is the standard Gaussian blur radius
* @return border instance so these calls can be chained
*/
public RoundBorder shadowBlur(float shadowBlur) {
this.shadowBlur = shadowBlur;
return this;
}
/**
* When set to true this border grows into a rectangle when the space isn't perfectly circular
* @param rectangle When set to true this border grows into a rectangle when the space isn't perfectly circular
* @return border instance so these calls can be chained
*/
public RoundBorder rectangle(boolean rectangle) {
this.rectangle = rectangle;
return this;
}
@Override
public void paintBorderBackground(Graphics g, Component c) {
int w = c.getWidth();
int h = c.getHeight();
int x = c.getX();
int y = c.getY();
if(w > 0 && h > 0) {
Image background = (Image)c.getClientProperty(CACHE_KEY + instanceVal);
if(background != null && background.getWidth() == w && background.getHeight() == h) {
g.drawImage(background, x, y);
return;
}
} else {
return;
}
Image target = Image.createImage(w, h, 0);
int shapeX = 0;
int shapeY = 0;
int shapeW = w;
int shapeH = h;
Graphics tg = target.getGraphics();
tg.setAntiAliased(true);
int shadowSpreadL = shadowSpread;
if(shadowMM) {
shadowSpreadL = Display.getInstance().convertToPixels(shadowSpreadL);
}
if(shadowOpacity > 0) {
shapeW -= shadowSpreadL;
shapeW -= (shadowBlur / 2);
shapeH -= shadowSpreadL;
shapeH -= (shadowBlur / 2);
shapeX += Math.round((shadowSpreadL + (shadowBlur / 2)) * shadowX);
shapeY += Math.round((shadowSpreadL + (shadowBlur / 2)) * shadowY);
// draw a gradient of sort for the shadow
for(int iter = shadowSpreadL - 1 ; iter >= 0 ; iter--) {
tg.translate(iter, iter);
fillShape(tg, 0, shadowOpacity / shadowSpreadL, w - (iter * 2), h - (iter * 2), false);
tg.translate(-iter, -iter);
}
if(Display.getInstance().isGaussianBlurSupported()) {
Image blured = Display.getInstance().gaussianBlurImage(target, shadowBlur/2);
target = Image.createImage(w, h, 0);
tg = target.getGraphics();
tg.drawImage(blured, 0, 0);
tg.setAntiAliased(true);
}
}
tg.translate(shapeX, shapeY);
if(uiid && tg.isShapeClipSupported()) {
c.getStyle().setBorder(Border.createEmpty());
GeneralPath gp = new GeneralPath();
if(rectangle) {
float sw = this.stroke != null ? this.stroke.getLineWidth() : 0;
gp.moveTo(shapeH / 2.0, sw);
gp.lineTo(shapeW - (shapeH / 2.0), sw);
gp.arcTo(shapeW - (shapeH / 2.0), shapeH / 2.0, shapeW - (shapeH / 2.0), shapeH-sw, true);
gp.lineTo(shapeH / 2.0, shapeH-sw);
gp.arcTo(shapeH / 2.0, shapeH / 2.0, shapeH / 2.0, sw, true);
gp.closePath();
} else {
int size = shapeW;
int xPos = 0;
int yPos = 0;
if(shapeW != shapeH) {
if(shapeW > shapeH) {
size = shapeH;
xPos = (shapeW - shapeH) / 2;
} else {
size = shapeW;
yPos = (shapeH - shapeW) / 2;
}
}
gp.arc(xPos, yPos, size, size, 0, 2*Math.PI);
}
tg.setClip(gp);
c.getStyle().getBgPainter().paint(tg, new Rectangle(0, 0, w, h));
c.getStyle().setBorder(this);
} else {
fillShape(tg, color, opacity, shapeW, shapeH, true);
}
g.drawImage(target, x, y);
c.putClientProperty(CACHE_KEY + instanceVal, target);
}
@Override
public int getMinimumHeight() {
return shadowSpread + Math.round(shadowBlur) + Display.getInstance().convertToPixels(1);
}
@Override
public int getMinimumWidth() {
return shadowSpread + Math.round(shadowBlur) + Display.getInstance().convertToPixels(1);
}
private void fillShape(Graphics g, int color, int opacity, int width, int height, boolean stroke) {
g.setColor(color);
g.setAlpha(opacity);
if(!rectangle || width <= height) {
int x = 0;
int y = 0;
int size = width;
if(width != height) {
if(width > height) {
size = height;
x = (width - height) / 2;
} else {
size = width;
y = (height - width) / 2;
}
}
if(stroke && this.stroke != null) {
int sw = (int)Math.ceil((stroke && this.stroke != null) ? this.stroke.getLineWidth() : 0);
GeneralPath arc = new GeneralPath();
arc.arc(x+sw/2, y+sw/2, size-2*sw, size-2*sw, 0, 2*Math.PI);
g.fillShape(arc);
g.setColor(strokeColor);
g.setAlpha(strokeOpacity);
g.drawShape(arc, this.stroke);
} else {
g.fillArc(x, y, size, size, 0, 360);
}
} else {
GeneralPath gp = new GeneralPath();
float sw = (stroke && this.stroke != null) ? this.stroke.getLineWidth() : 0;
gp.moveTo(height / 2.0, sw);
gp.lineTo(width - (height / 2.0), sw);
gp.arcTo(width - (height / 2.0), height / 2.0, width - (height / 2.0), height-sw, true);
gp.lineTo(height / 2.0, height-sw);
gp.arcTo(height / 2.0, height / 2.0, height / 2.0, sw, true);
gp.closePath();
g.fillShape(gp);
if(stroke && this.stroke != null) {
g.setAlpha(strokeOpacity);
g.setColor(strokeColor);
g.drawShape(gp, this.stroke);
}
}
}
@Override
public boolean isBackgroundPainter() {
return true;
}
/**
* The color of the border background
* @return the color
*/
public int getColor() {
return color;
}
/**
* The opacity (transparency) of the border background
* @return the opacity
*/
public int getOpacity() {
return opacity;
}
/**
* The color of the edge of the border if applicable
* @return the strokeColor
*/
public int getStrokeColor() {
return strokeColor;
}
/**
* The opacity of the edge of the border if applicable
* @return the strokeOpacity
*/
public int getStrokeOpacity() {
return strokeOpacity;
}
/**
* The thickness of the edge of the border if applicable, 0 if no stroke is needed
* @return the strokeThickness
*/
public float getStrokeThickness() {
return strokeThickness;
}
/**
* True if the thickness of the stroke is in millimeters
* @return the strokeMM
*/
public boolean isStrokeMM() {
return strokeMM;
}
/**
* The spread of the shadow in pixels of millimeters
* @return the shadowSpread
*/
public int getShadowSpread() {
return shadowSpread;
}
/**
* The opacity of the shadow between 0 and 255
* @return the shadowOpacity
*/
public int getShadowOpacity() {
return shadowOpacity;
}
/**
* X axis bias of the shadow between 0 and 1 where 0 is to the top and 1 is to the bottom, defaults to 0.5
* @return the shadowX
*/
public float getShadowX() {
return shadowX;
}
/**
* Y axis bias of the shadow between 0 and 1 where 0 is to the left and 1 is to the right, defaults to 0.5
* @return the shadowY
*/
public float getShadowY() {
return shadowY;
}
/**
* The Gaussian blur size
* @return the shadowBlur
*/
public float getShadowBlur() {
return shadowBlur;
}
/**
* True if the shadow spread is in millimeters
* @return the shadowMM
*/
public boolean isShadowMM() {
return shadowMM;
}
/**
* True if this border grows into a rectangle horizontally or keeps growing as a circle
* @return the rectangle
*/
public boolean isRectangle() {
return rectangle;
}
@Override
public int hashCode() {
int hash = 5;
hash = 43 * hash + this.color;
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final RoundBorder other = (RoundBorder) obj;
if (this.color != other.color) {
return false;
}
if (this.opacity != other.opacity) {
return false;
}
if (this.strokeColor != other.strokeColor) {
return false;
}
if (this.strokeOpacity != other.strokeOpacity) {
return false;
}
if (this.strokeThickness != other.strokeThickness) {
return false;
}
if (this.strokeMM != other.strokeMM) {
return false;
}
if (this.shadowSpread != other.shadowSpread) {
return false;
}
if (this.shadowOpacity != other.shadowOpacity) {
return false;
}
if (this.shadowX != other.shadowX) {
return false;
}
if (this.shadowY != other.shadowY) {
return false;
}
if (this.shadowBlur != other.shadowBlur) {
return false;
}
if (this.shadowMM != other.shadowMM) {
return false;
}
if (this.rectangle != other.rectangle) {
return false;
}
return true;
}
}