---
category: swing
folder: FocusBorderAnimation
title: JTextFieldがFocusを取得したらBorderの右上左辺を順に描画する
tags: [Border, Animation, FocusListener, JTextField]
author: aterai
pubdate: 2021-01-11T07:46:47+09:00
description: JTextFieldがキーボードフォーカスを取得したら右上左辺を直線で順に描画するBorderアニメーションを開始します。
image: https://drive.google.com/uc?id=1QFbs_eWetOYY37ic7VLmBwgRoGOTIXxY
hreflang:
    href: https://java-swing-tips.blogspot.com/2021/01/draw-upper-right-left-and-right-borders.html
    lang: en
---
* 概要 [#summary]
`JTextField`がキーボードフォーカスを取得したら右上左辺を直線で順に描画する`Border`アニメーションを開始します。

#download(https://drive.google.com/uc?id=1QFbs_eWetOYY37ic7VLmBwgRoGOTIXxY)

* サンプルコード [#sourcecode]
#code(link){{
class AnimatedBorder extends EmptyBorder {
  private static final int BOTTOM_SPACE = 20;
  private static final double PLAY_TIME = 300d;
  private static final int BORDER = 4;
  private final Timer animator = new Timer(10, null);
  private final transient Stroke stroke = new BasicStroke(BORDER);
  private final transient Stroke bottomStroke = new BasicStroke(BORDER / 2f);
  private long startTime = -1L;
  private final transient List<Point2D> points = new ArrayList<>();
  private transient Shape shape;

  public AnimatedBorder(JComponent c) {
    super(BORDER, BORDER, BORDER + BOTTOM_SPACE, BORDER);
    animator.addActionListener(e -> {
      if (startTime < 0) {
        startTime = System.currentTimeMillis();
      }
      long playTime = System.currentTimeMillis() - startTime;
      double progress = playTime / PLAY_TIME;
      boolean stop = progress > 1d || points.isEmpty();
      if (stop) {
        startTime = -1L;
        ((Timer) e.getSource()).stop();
        c.repaint();
        return;
      }
      Point2D pos = new Point2D.Double();
      pos.setLocation(points.get(0));
      Path2D border = new Path2D.Double();
      border.moveTo(pos.getX(), pos.getY());
      int idx = Math.min(Math.max(0, (int) (points.size() * progress)), points.size() - 1);
      int idx = Math.min(Math.max(
          0, (int) (points.size() * progress)), points.size() - 1);
      for (int i = 0; i <= idx; i++) {
        pos.setLocation(points.get(i));
        border.lineTo(pos.getX(), pos.getY());
        border.moveTo(pos.getX(), pos.getY());
      }
      border.closePath();
      shape = border;
      c.repaint();
    });
    c.addFocusListener(new FocusListener() {
      @Override public void focusGained(FocusEvent e) {
        Rectangle r = c.getBounds();
        r.height -= BOTTOM_SPACE + 1;
        Path2D p = new Path2D.Double();
        p.moveTo(r.getWidth(), r.getHeight());
        p.lineTo(r.getWidth(), 0d);
        p.lineTo(0d, 0d);
        p.lineTo(0d, r.getHeight());
        p.closePath();
        makePointList(p, points);
        animator.start();
      }

      @Override public void focusLost(FocusEvent e) {
        points.clear();
        shape = null;
        c.repaint();
      }
    });
  }

  @Override public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) {
  @Override public void paintBorder(
        Component c, Graphics g, int x, int y, int w, int h) {
    super.paintBorder(c, g, x, y, w, h);
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setPaint(c.isEnabled() ? Color.ORANGE : Color.GRAY);
    g2.translate(x, y);
    g2.setStroke(bottomStroke);
    g2.drawLine(0, h - BOTTOM_SPACE, w - 1, h - BOTTOM_SPACE);
    g2.setStroke(stroke);
    if (shape != null) {
      g2.draw(shape);
    }
    g2.dispose();
  }

  private static void makePointList(Shape shape, List<Point2D> points) {
    points.clear();
    PathIterator pi = shape.getPathIterator(null, .01);
    Point2D prev = new Point2D.Double();
    double delta = .02;
    double threshold = 2d;
    double[] coords = new double[6];
    while (!pi.isDone()) {
      int segment = pi.currentSegment(coords);
      Point2D current = createPoint(coords[0], coords[1]);
      if (segment == PathIterator.SEG_MOVETO) {
        points.add(current);
        prev.setLocation(current);
      } else if (segment == PathIterator.SEG_LINETO) {
        double distance = prev.distance(current);
        double fraction = delta;
        if (distance > threshold) {
          Point2D p = interpolate(prev, current, fraction);
          while (distance > prev.distance(p)) {
            points.add(p);
            fraction += delta;
            p = interpolate(prev, current, fraction);
          }
        } else {
          points.add(current);
        }
        prev.setLocation(current);
      }
      pi.next();
    }
  }

  private static Point2D createPoint(double x, double y) {
    return new Point2D.Double(x, y);
  }

  private static Point2D interpolate(Point2D start, Point2D end, double fraction) {
  private static Point2D interpolate(
        Point2D start, Point2D end, double fraction) {
    double dx = end.getX() - start.getX();
    double dy = end.getY() - start.getY();
    double nx = start.getX() + dx * fraction;
    double ny = start.getY() + dy * fraction;
    return new Point2D.Double(nx, ny);
  }
}
}}

* 解説 [#explanation]
- フォーカスなし
-- `Color.ORANGE`の下線を描画
- 選択不可
-- `Color.GRAY`の下線を描画
- フォーカスあり
-- `FocusListener#focusGained(...)`で`Timer`を起動
-- `Path2D.Double()`で`JTextField`の右上左辺を表す折れ線を作成
-- 折れ線をその上に存在する点の`ArrayList`に変換
--- [[Shapeから取得したPathIteratorに沿って図形を移動する>Swing/MotionPathAnimation]]
-- `Timer`でインデックスを更新し`ArrayList`の始点からインデックス番目の点を辿るパス(`Path2D.Double()`)を生成
-- `paintBorder(...)`をオーバーライドして上記のパスを`Color.ORANGE`で描画

* 参考リンク [#reference]
- [[Borderのアニメーション>Swing/RippleBorder]]
- [[Shapeから取得したPathIteratorに沿って図形を移動する>Swing/MotionPathAnimation]]

* コメント [#comment]
#comment
#comment