Summary

JTableのセル選択領域全体の縁に破線フェーズの異なる破線を交互に切り替えることでその移動アニメーションを描画します。

Source Code Examples

class TranslucentCellSelectionLayerUI extends LayerUI<JScrollPane> {
  private static final float WIDTH = 2f;
  private static final float MITER = 5f;
  private static final float[] DASH = {4f, 2f};
  private static final Stroke BORDER_STROKE1 = new BasicStroke(
      WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, MITER, DASH, 0f);
  private static final Stroke BORDER_STROKE2 = new BasicStroke(
      WIDTH, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, MITER, DASH, 2f);
  private static Stroke borderStroke = BORDER_STROKE1;
  private JTable table;
  private boolean flg;
  private final Timer animator = new Timer(480, e -> {
    if (table != null && !table.isEditing()) {
      borderStroke = flg ? BORDER_STROKE1 : BORDER_STROKE2;
      repaintSelectedArea(table);
      flg = !flg;
    }
  });

  @Override public void installUI(JComponent c) {
    super.installUI(c);
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(AWTEvent.FOCUS_EVENT_MASK);
    }
  }

  @Override public void uninstallUI(JComponent c) {
    if (c instanceof JLayer) {
      ((JLayer<?>) c).setLayerEventMask(0);
    }
    super.uninstallUI(c);
  }

  @Override protected void processFocusEvent(
        FocusEvent e, JLayer<? extends JScrollPane> l) {
    table = getTable(l);
    if (e.getID() == FocusEvent.FOCUS_GAINED) {
      animator.start();
    } else {
      animator.stop();
    }
    super.processFocusEvent(e, l);
  }

  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    JTable table = getTable(c);
    int cc = table.getSelectedColumnCount();
    int rc = table.getSelectedRowCount();
    if (cc != 0 && rc != 0 && !table.isEditing()) {
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(
          RenderingHints.KEY_ANTIALIASING,
          RenderingHints.VALUE_ANTIALIAS_ON);
      Area area = new Area();
      getSelectedArea(table).forEach(r -> {
        Rectangle rect = SwingUtilities.convertRectangle(table, r, c);
        area.add(new Area(rect));
      });
      Dimension ics = table.getIntercellSpacing();
      Color v = table.getSelectionBackground();
      Color sbc = new Color(v.getRed(), v.getGreen(), v.getBlue(), 0x32);
      for (Area a : GeomUtils.singularization(area)) {
        Rectangle r = a.getBounds();
        r.width -= ics.width - 1;
        r.height -= ics.height - 1;
        g2.setPaint(sbc);
        g2.fill(r);
        g2.setPaint(v);
        g2.setStroke(borderStroke);
        g2.draw(r);
      }
      g2.dispose();
    }
  }

  private static List<Rectangle> getSelectedArea(JTable tbl) {
    List<Rectangle> list = new ArrayList<>();
    for (int row : tbl.getSelectedRows()) {
      for (int col : tbl.getSelectedColumns()) {
        if (tbl.isCellSelected(row, col)) {
          list.add(tbl.getCellRect(row, col, true));
        }
      }
    }
    return list;
  }

  private static JTable getTable(Component c) {
    JTable table = null;
    if (c instanceof JLayer) {
      Component c1 = ((JLayer<?>) c).getView();
      if (c1 instanceof JScrollPane) {
        table = (JTable) ((JScrollPane) c1).getViewport().getView();
      }
    }
    return table;
  }

  private static void repaintSelectedArea(JTable tbl) {
    int cc = tbl.getSelectedColumnCount();
    int rc = tbl.getSelectedRowCount();
    if (cc != 0 && rc != 0) {
      Area area = new Area();
      getSelectedArea(tbl).forEach(r -> area.add(new Area(r)));
      tbl.repaint(area.getBounds());
    }
  }
}
View in GitHub: Java, Kotlin

Description

  • JTableのセル選択を半透明化して上書きと同様にJTable側の選択背景色は完全に透明、選択文字色は文字色と同一に設定し、セル選択領域やその縁の描画はJLayerを適用してLayerUI#paint(...)メソッド内で実行するよう設定
  • 破線フェーズのみ2fずらしたBasicStroke2つ用意
    • セル選択領域が存在しかつJTableにフォーカスが存在する場合はTimerを起動してこれらを入れ替えて描画することで破線の移動アニメーションを実行

Reference

Comment