Skip to content

Instantly share code, notes, and snippets.

@wenerme
Last active March 4, 2019 02:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wenerme/6940534 to your computer and use it in GitHub Desktop.
Save wenerme/6940534 to your computer and use it in GitHub Desktop.
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