Summary
JButtonに円形の切り抜きアイコンを設定し、これらの左右がデフォルトでは重なり、マウスオーバー時に水平方向に拡大するようレイアウトされたアバターグループを作成します。
Screenshot

Advertisement
Source Code Examples
class StackedLayout implements LayoutManager {
private double gapFraction;
public StackedLayout(double gapFraction) {
this.gapFraction = gapFraction;
}
public void setGapFraction(double gapFraction) {
this.gapFraction = gapFraction;
}
@Override public void layoutContainer(Container parent) {
int n = parent.getComponentCount();
if (n > 0) {
Insets insets = parent.getInsets();
int x = insets.left;
int y = insets.top;
for (int i = 0; i < n; i++) {
Component c = parent.getComponent(i);
Dimension d = c.getPreferredSize();
c.setBounds(x, y, d.width, d.height);
// Step calc: 60% of width as default overlap, 40% as animated spread
int step = (int) (d.width * .6 + (d.width * .4 * gapFraction));
x += step;
}
}
}
@Override public Dimension preferredLayoutSize(Container parent) {
Dimension size = new Dimension();
int n = parent.getComponentCount();
if (n > 0) {
int totalWidth = 0;
int maxHeight = 0;
for (int i = 0; i < n; i++) {
Component c = parent.getComponent(i);
Dimension d = c.getPreferredSize();
maxHeight = Math.max(maxHeight, d.height);
if (i < n - 1) {
// Add overlap for all but the last component
int step = (int) (d.width * .6 + (d.width * .4 * gapFraction));
totalWidth += step;
} else {
totalWidth += d.width;
}
}
Insets insets = parent.getInsets();
totalWidth += insets.left + insets.right;
maxHeight += insets.top + insets.bottom;
size.setSize(totalWidth, maxHeight);
}
return size;
}
@Override public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
@Override public void addLayoutComponent(String name, Component comp) {
// empty
}
@Override public void removeLayoutComponent(Component comp) {
// empty
}
}
View in GitHub: Java, KotlinDescription
- アバター
JButtonの重なり状態を調整可能なStackedLayoutを作成StackedLayoutはLayoutManager#layoutContainer(...)を実装し、デフォルトの重なり幅は60%、アニメーションによる広がり幅は40%で各アバターJButtonを配置可能- 上: 左側のアバター
JButtonを手前のレイヤに表示するレイアウト - 下: 右側のアバター
JButtonを手前のレイヤに表示するレイアウト
- アバター画像を円形切り抜きして
JButtonに表示- アバター画像(このサンプルでは任意のサイズのカラー
Icon)を円図形で切り抜き、サイズを一定に揃えてJButtonに配置 Graphics#setClip(...)で円形に切り抜くと縁にジャギーが発生するので、JPanelに色相環を描画すると同様のソフトクリッピングで縁をなめらかにしているJButton#getToolTipLocation(...)をオーバーライドして、JTabbedPaneのツールヒントをタブ位置に対応したふきだしに変更すると同様に中央にふきだしのしっぽが存在するJToolTipをJButtonの中央上部に表示するよう位置を調整- マウスクリックやマウスオーバーで反応する領域を
JButton#contains(...)をオーバーライドして円形に制限
- アバター画像(このサンプルでは任意のサイズのカラー
class AvatarButton extends JButton {
private static final int DIAMETER = 24;
private static final Insets INSETS = new Insets(2, 2, 2, 2);
private transient JToolTip tip;
public AvatarButton(Icon icon) {
super(icon);
}
@Override public void updateUI() {
super.updateUI();
setContentAreaFilled(false);
setBorderPainted(false);
setFocusPainted(false);
}
@Override public Dimension getPreferredSize() {
int w = DIAMETER + INSETS.left + INSETS.right;
int h = DIAMETER + INSETS.top + INSETS.bottom;
return new Dimension(w, h);
}
@Override protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int w = getWidth();
int h = getHeight();
// 1. Draw the border with background color
g2.setColor(getParent().getBackground());
g2.fill(new Ellipse2D.Double(0, 0, w, h));
// 2. Render with Soft Clipping
GraphicsConfiguration gc = g2.getDeviceConfiguration();
BufferedImage buffer = gc.createCompatibleImage(
w, h, Transparency.TRANSLUCENT);
Graphics2D g2d = buffer.createGraphics();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setComposite(AlphaComposite.Src);
g2d.fill(new Ellipse2D.Double(INSETS.left, INSETS.top, DIAMETER, DIAMETER));
// Composite icon inside the circle using SrcAtop
g2d.setComposite(AlphaComposite.SrcAtop);
// Scale icon to fit the circle
Icon icon = getIcon();
double scale = (double) DIAMETER / Math.max(icon.getIconWidth(), icon.getIconHeight());
AffineTransform at = AffineTransform.getTranslateInstance(
INSETS.left, INSETS.top);
at.scale(scale, scale);
g2d.transform(at);
icon.paintIcon(this, g2d, 0, 0);
g2d.dispose();
g2.drawImage(buffer, 0, 0, null);
g2.dispose();
}
@Override public boolean contains(int x, int y) {
int w = getWidth();
int h = getHeight();
Ellipse2D circle = new Ellipse2D.Double(0, 0, w, h);
return circle.contains(x, y);
}
@Override public JToolTip createToolTip() {
if (tip == null) {
tip = new BalloonToolTip();
tip.setComponent(this);
}
return tip;
}
@Override public Point getToolTipLocation(MouseEvent e) {
return Optional.ofNullable(getToolTipText(e)).map(toolTipText -> {
JToolTip toolTip = createToolTip();
toolTip.setTipText(toolTipText);
Rectangle buttonBounds = SwingUtilities.calculateInnerArea(this, null);
Dimension tooltipSize = toolTip.getPreferredSize();
int centerX = (int) (buttonBounds.getCenterX() - tooltipSize.getWidth() / 2d);
int topY = buttonBounds.y - tooltipSize.height - 2;
return new Point(centerX, topY);
}).orElse(null);
}
}
- マウスイベント検知用
JLayer- 各
JButtonやそれらを配置した親JPanelなどにMouseListenerを設定するとマウスの出入りの制御が複雑になるので、JLayerでラップして展開・縮小アニメーションの開始、終了をLayerUI#processMouseEvent(...)で実行している - 展開・縮小アニメーションのフレーム更新は
LayerUI内に作成したTimerを使用し、StackedLayoutの重なり状態を更新することで実行している
- 各
class AvatarLayerUI extends LayerUI<JPanel> {
private final Timer timer = new Timer(15, null);
private double currentFraction;
private double targetFraction;
@Override public void installUI(JComponent c) {
super.installUI(c);
JLayer<?> l = (JLayer<?>) c;
l.setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK);
timer.addActionListener(e -> animation((JPanel) l.getView()));
}
// Ease-Out: Moves 25% closer to target per frame
private void animation(JPanel panel) {
double diff = targetFraction - currentFraction;
boolean isEnd = Math.abs(diff) < .1;
if (isEnd) {
currentFraction = targetFraction;
timer.stop();
} else {
currentFraction += diff * .25;
}
StackedLayout layout = (StackedLayout) panel.getLayout();
layout.setGapFraction(currentFraction);
panel.revalidate();
panel.repaint();
}
@Override protected void processMouseEvent(MouseEvent e, JLayer<? extends JPanel> l) {
if (e.getID() == MouseEvent.MOUSE_ENTERED) {
startAnimation(1d);
} else if (e.getID() == MouseEvent.MOUSE_EXITED) {
startAnimation(0d);
}
}
private void startAnimation(double target) {
this.targetFraction = target;
if (!timer.isRunning()) {
timer.start();
}
}
}
Reference
- JToggleButtonをFlowLayoutで重ねて表示する
- ImageIconの形でJButtonを作成
- JPanelに色相環を描画する
- JTabbedPaneのツールヒントをタブ位置に対応したふきだしに変更する