A TextBubbleBorder for Java Swing. Inspire by this http://stackoverflow.com/questions/8462414/internal-padding-for-jtextarea-with-background-image/8463742#8463742. Add more features. Use the shortcuts to control the demo. Download: http://resources.wener.me/java/TextBubbleBorderDemo.jar
import java.awt.BasicStroke; | |
import java.awt.Color; | |
import java.awt.Component; | |
import java.awt.DisplayMode; | |
import java.awt.Graphics; | |
import java.awt.Graphics2D; | |
import java.awt.GraphicsEnvironment; | |
import java.awt.Insets; | |
import java.awt.Polygon; | |
import java.awt.Rectangle; | |
import java.awt.RenderingHints; | |
import java.awt.event.KeyEvent; | |
import java.awt.event.KeyListener; | |
import java.awt.geom.Area; | |
import java.awt.geom.RoundRectangle2D; | |
import javax.swing.JFrame; | |
import javax.swing.JLabel; | |
import javax.swing.JTextArea; | |
import javax.swing.SwingConstants; | |
import javax.swing.SwingUtilities; | |
import javax.swing.border.AbstractBorder; | |
public class TextBubbleBorderDemo extends JFrame implements KeyListener | |
{ | |
private static final long serialVersionUID = 1L; | |
public static void main(String[] args) | |
{ | |
SwingUtilities.invokeLater(new Runnable() | |
{ | |
public void run() | |
{ | |
try | |
{ | |
TextBubbleBorderDemo frame = new TextBubbleBorderDemo(); | |
frame.setVisible(true); | |
} catch (Exception e) | |
{ | |
e.printStackTrace(); | |
} | |
} | |
}); | |
} | |
JLabel label; | |
TextBubbleBorder border; | |
TextBubbleBorderDemo() | |
{ | |
setTitle("TextBubbleBorderDemo"); | |
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); | |
label = new JLabel("The quick brown fox jumped over the lazy dog!"); | |
border = new TextBubbleBorder(Color.MAGENTA.darker(),2,8,10); | |
label.setBorder(border); | |
label.setOpaque(true); | |
label.setSize(label.getPreferredSize()); | |
label.setLocation(250,50); | |
add(label); | |
JTextArea field; | |
field = new JTextArea (); | |
field.setText("Arrow key: chang arrow size\n " | |
+ "Q/A: pointer size\n " | |
+ "W/S: thinkness\n " | |
+ "E/D: pointer padding percent\n " | |
+ "R/F: radius\n" | |
+ "C: toggle color\n" | |
+ "http://blog.wener.me"); | |
field.setEditable(false); | |
field.setSize(200,200); | |
add(field); | |
setLayout(null); | |
setSize(600, 200); | |
// 居中窗口 | |
{ | |
DisplayMode mode = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDisplayMode(); | |
setLocation((mode.getWidth() - getWidth())/2, (mode.getHeight() - getHeight())/2); | |
} | |
addKeyListener(this); | |
setFocusable(true); | |
requestFocus(); | |
} | |
@Override | |
public void keyPressed(KeyEvent e) | |
{ | |
switch (e.getKeyCode()) | |
{ | |
case KeyEvent.VK_UP: | |
border.setPointerSide(SwingConstants.TOP); | |
break; | |
case KeyEvent.VK_DOWN: | |
border.setPointerSide(SwingConstants.BOTTOM); | |
break; | |
case KeyEvent.VK_LEFT: | |
border.setPointerSide(SwingConstants.LEFT); | |
break; | |
case KeyEvent.VK_RIGHT: | |
border.setPointerSide(SwingConstants.RIGHT); | |
break; | |
case KeyEvent.VK_Q: | |
border.setPointerSize(border.getPointerSize()+2); | |
break; | |
case KeyEvent.VK_A: | |
border.setPointerSize(border.getPointerSize()-2); | |
break; | |
case KeyEvent.VK_W: | |
border.setThickness(border.getThickness()+2); | |
break; | |
case KeyEvent.VK_S: | |
border.setThickness(border.getThickness()-2); | |
break; | |
case KeyEvent.VK_E: | |
border.setPointerPadPercent(border.getPointerPadPercent()+0.1); | |
break; | |
case KeyEvent.VK_D: | |
border.setPointerPadPercent(border.getPointerPadPercent()-0.1); | |
break; | |
case KeyEvent.VK_R: | |
border.setRadius(border.getRadius()+2); | |
break; | |
case KeyEvent.VK_F: | |
border.setRadius(border.getRadius()-2); | |
break; | |
case KeyEvent.VK_C: | |
if (border.getColor() == Color.cyan) | |
border.setColor(Color.MAGENTA.darker()); | |
else | |
border.setColor(Color.cyan); | |
break; | |
default: | |
break; | |
} | |
label.setSize(label.getPreferredSize()); | |
repaint(); | |
} | |
@Override | |
public void keyReleased(KeyEvent e) | |
{ | |
// TODO Auto-generated method stub | |
} | |
@Override | |
public void keyTyped(KeyEvent e) | |
{ | |
// TODO Auto-generated method stub | |
} | |
} | |
class TextBubbleBorder extends AbstractBorder | |
{ | |
private static final long serialVersionUID = 1L; | |
private Color color; | |
private int thickness; | |
private int radius; | |
private int pointerSize = 0; | |
private Insets insets = null; | |
private BasicStroke stroke = null; | |
private int strokePad; | |
private double pointerPadPercent = 0.5; | |
int pointerSide = SwingConstants.TOP; | |
RenderingHints hints; | |
TextBubbleBorder(Color color) | |
{ | |
this(color, 2, 4, 0); | |
} | |
TextBubbleBorder(Color color, int thickness, int radius, int pointerSize) | |
{ | |
this.color = color; | |
this.thickness = thickness; | |
this.radius = radius; | |
this.pointerSize = pointerSize; | |
hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); | |
insets = new Insets(0, 0, 0, 0); | |
setThickness(thickness); | |
} | |
// {{ Setter/Getter | |
public Color getColor() | |
{ | |
return color; | |
} | |
public TextBubbleBorder setColor(Color color) | |
{ | |
this.color = color; | |
return this; | |
} | |
public double getPointerPadPercent() | |
{ | |
return pointerPadPercent; | |
} | |
public TextBubbleBorder setPointerPadPercent(double percent) | |
{ | |
this.pointerPadPercent = percent > 1? 1: percent; | |
pointerPadPercent = pointerPadPercent < 0? 0: pointerPadPercent; | |
return this; | |
} | |
public int getThickness() | |
{ | |
return thickness; | |
} | |
public int getRadius() | |
{ | |
return radius; | |
} | |
public int getPointerSize() | |
{ | |
return pointerSize; | |
} | |
public TextBubbleBorder setThickness(int n) | |
{ | |
thickness = n < 0? 0: n; | |
stroke = new BasicStroke(thickness); | |
strokePad = thickness / 2; | |
setPointerSize(pointerSize); | |
return this; | |
} | |
public TextBubbleBorder setPointerSize(int size) | |
{ | |
pointerSize = size < 0 ? 0: size; | |
// 这里需要 radii/2 不然会有多出来的边距 | |
int pad = radius/2 + strokePad; | |
int pointerSidePad = pad + pointerSize + strokePad; | |
// 根据不同的方向设置不同的Padding | |
int left, right, bottom, top; | |
left = right = bottom = top = pad; | |
switch (pointerSide) | |
{ | |
case SwingConstants.TOP: | |
top = pointerSidePad; | |
break; | |
case SwingConstants.LEFT: | |
left = pointerSidePad; | |
break; | |
case SwingConstants.RIGHT: | |
right = pointerSidePad; | |
break; | |
default: | |
case SwingConstants.BOTTOM: | |
bottom = pointerSidePad; | |
break; | |
} | |
insets.set(top, left, bottom, right); | |
return this; | |
} | |
public int getPointerSide() | |
{ | |
return pointerSide; | |
} | |
public TextBubbleBorder setPointerSide(int pointerSide) | |
{ | |
this.pointerSide = pointerSide; | |
setPointerSize(pointerSize); | |
return this; | |
} | |
public TextBubbleBorder setRadius(int radius) | |
{ | |
this.radius = radius; | |
setPointerSize(pointerSize); | |
return this; | |
} | |
// }} | |
@Override | |
public Insets getBorderInsets(Component c) | |
{ | |
return insets; | |
} | |
@Override | |
public Insets getBorderInsets(Component c, Insets insets) | |
{ | |
return getBorderInsets(c); | |
} | |
@Override | |
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) | |
{ | |
Graphics2D g2 = (Graphics2D) g; | |
// 修正背景色的问题 | |
g2.setBackground(c.getBackground()); | |
int bottomLineY = height - thickness - pointerSize; | |
RoundRectangle2D.Double bubble; | |
Polygon pointer = new Polygon(); | |
// 初始范围 | |
{ | |
// 设置圆角矩形 | |
int rx, ry, rw, rh; | |
rx = ry = strokePad; | |
rw = width - thickness; | |
rh = height - thickness; | |
switch (pointerSide) | |
{ | |
case SwingConstants.LEFT: | |
rx += pointerSize; | |
case SwingConstants.RIGHT: | |
rw -= pointerSize; | |
break; | |
case SwingConstants.TOP: | |
ry += pointerSize; | |
case SwingConstants.BOTTOM: | |
default: | |
rh -= pointerSize; | |
break; | |
} | |
bubble = new RoundRectangle2D.Double(rx,ry,rw,rh,radius, radius); | |
// 计算偏移 | |
int pointerPad; | |
if (pointerSide == SwingConstants.LEFT || pointerSide == SwingConstants.RIGHT) | |
{ | |
pointerPad = (int) (pointerPadPercent * (height-radius*2-pointerSize)); | |
}else { | |
pointerPad = (int) (pointerPadPercent * (width-radius*2-pointerSize)); | |
} | |
// 设置三角 | |
int basePad = strokePad + radius + pointerPad; | |
switch (pointerSide) | |
{ | |
case SwingConstants.LEFT: | |
pointer.addPoint(rx, basePad);// top | |
pointer.addPoint(rx, basePad+pointerSize);// bottom | |
pointer.addPoint(strokePad, basePad+pointerSize/2); | |
break; | |
case SwingConstants.RIGHT: | |
pointer.addPoint(rw, basePad);// top | |
pointer.addPoint(rw, basePad+pointerSize);// bottom | |
pointer.addPoint(width-strokePad, basePad+pointerSize/2); | |
break; | |
case SwingConstants.TOP: | |
pointer.addPoint(basePad, ry);// left | |
pointer.addPoint(basePad + pointerSize, ry);// right | |
pointer.addPoint(basePad + (pointerSize / 2), strokePad); | |
break; | |
default: | |
case SwingConstants.BOTTOM: | |
pointer.addPoint(basePad, rh);// left | |
pointer.addPoint(basePad + pointerSize, rh);// right | |
pointer.addPoint(basePad + (pointerSize / 2), height - strokePad); | |
break; | |
} | |
} | |
Area area = new Area(bubble); | |
area.add(new Area(pointer)); | |
g2.setRenderingHints(hints); | |
Area spareSpace = new Area(new Rectangle(0, 0, width, height)); | |
spareSpace.subtract(area); | |
g2.setClip(spareSpace); | |
g2.clearRect(0, 0, width, height); | |
g2.setClip(null); | |
g2.setColor(color); | |
g2.setStroke(stroke); | |
g2.draw(area); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment