---
category: swing
folder: TreeNodeExpandCollapseAnimations
title: JTreeにノード展開、折り畳みアニメーションを実装する
tags: [JTree, Animation, TreeWillExpandListener]
author: aterai
pubdate: 2024-07-22T02:27:36+09:00
description: JTreeのノード展開、折り畳みでその子ノードの高さを増減するアニメーションを実行します。
image: https://drive.google.com/uc?id=1hHsf6k4Zt-UnrvuI-GIZWmu8NDCd6Yii
hreflang:
    href: https://java-swing-tips.blogspot.com/2024/07/animates-effect-of-expanding-and.html
    lang: en
---
* 概要 [#summary]
`JTree`のノード展開、折り畳みでその子ノードの高さを増減するアニメーションを実行します。

#download(https://drive.google.com/uc?id=1hHsf6k4Zt-UnrvuI-GIZWmu8NDCd6Yii)

* サンプルコード [#sourcecode]
#code(link){{
JTree tree = new JTree() {
  @Override public void updateUI() {
    super.updateUI();
    setRowHeight(-1);
    setCellRenderer(new HeightTreeCellRenderer());
  }
};
tree.addTreeWillExpandListener(new TreeWillExpandListener() {
  @Override public void treeWillExpand(TreeExpansionEvent e) {
    Object o = e.getPath().getLastPathComponent();
    if (o instanceof DefaultMutableTreeNode) {
      DefaultMutableTreeNode parent = (DefaultMutableTreeNode) o;
      List<DefaultMutableTreeNode> list = getTreeNodes(parent);
      parent.setUserObject(makeUserObject(parent, END_HEIGHT));
      list.forEach(n -> n.setUserObject(makeUserObject(n, START_HEIGHT)));
      startExpandTimer(e, list);
    }
  }

  @Override public void treeWillCollapse(TreeExpansionEvent e)
      throws ExpandVetoException {
    Object c = e.getPath().getLastPathComponent();
    if (c instanceof DefaultMutableTreeNode) {
      DefaultMutableTreeNode p = (DefaultMutableTreeNode) o;
      List<DefaultMutableTreeNode> list = getTreeNodes(p);
      boolean b = list
          .stream()
          .anyMatch(n -> {
            Object obj = n.getUserObject();
            return obj instanceof SizeNode
              && ((SizeNode) obj).height == END_HEIGHT;
          });
      if (b) {
        startCollapseTimer(e, list);
        throw new ExpandVetoException(e);
      }
    }
  }
});

class HeightTreeCellRenderer extends DefaultTreeCellRenderer {
  @Override public Component getTreeCellRendererComponent(
      JTree tree,
      Object value,
      boolean selected,
      boolean expanded,
      boolean leaf,
      int row,
      boolean hasFocus) {
    Component c = super.getTreeCellRendererComponent(
        tree, value, selected, expanded, leaf, row, hasFocus);
    DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
    Object uo = node.getUserObject();
    if (c instanceof JLabel && uo instanceof SizeNode) {
      JLabel l = (JLabel) c;
      SizeNode n = (SizeNode) uo;
      l.setPreferredSize(null); // reset prev preferred size
      l.setText(n.label); // recalculate preferred size
      Dimension d = l.getPreferredSize();
      d.height = n.height;
      l.setPreferredSize(d);
    }
    return c;
  }
}

class SizeNode {
  public final String label;
  public final int height;

  protected SizeNode(String label, int height) {
    this.label = label;
    this.height = height;
  }

  @Override public String toString() {
    return label;
  }
}
}}

* 解説 [#explanation]
- `JTree#setRowHeight(-1)`を設定して各表示行の高さを固定サイズではなくセルレンダラーの推奨サイズの高さを使用するよう設定
-- [[JTreeのノードを名前で検索して表示のフィルタリングを行う>Swing/TreeNodeFilter]]
- セルレンダラーの推奨サイズを`DefaultMutableTreeNode`のユーザーオブジェクトで指定した高さを指定することで展開・折り畳み中のアニメーションを実行
-- ユーザーオブジェクトの設定は`DefaultMutableTreeNode#setUserObject(obj)`ではなく`TreeModel#valueForPathChanged(path, obj)`を使用しないと`TreeModelListener`などにノード更新イベントが伝わらない
--- [[JTreeのノード追加、削除>Swing/AddNode]]
-- `JLabel#setPreferredSize(null)`を実行してからノードの文字列を設定しないと推奨サイズの幅が正しく取得できない
-- `JTree`のノードサイズキャッシュが影響している?
- `JTree`に`TreeWillExpandListener`を追加してノードの展開・折り畳み前にその子ノードの高さを増減するアニメーションを実行
-- `TreeWillExpandListener#treeWillExpand(...)`で展開前に子ノードすべての高さを縮小してから、`Timer`をスタートして段階的に元の高さに戻すことでアニメーションを表現
-- `TreeWillExpandListener#treeWillCollapse(...)`で子ノードの高さが元の高さの場合は、一旦`throw new ExpandVetoException(...)`で折り畳みを中止してから`Timer`をスタートして段階的に高さを縮小するアニメーションを実行し、一定の高さまで縮小されたら`JTree#collapsePath(TreePath)`で再度アニメーションなしの折り畳みを実行

#code{{
private static void startExpandTimer(
    TreeExpansionEvent e, List<DefaultMutableTreeNode> list) {
  JTree tree = (JTree) e.getSource();
  TreeModel model = tree.getModel();
  AtomicInteger height = new AtomicInteger(START_HEIGHT);
  new Timer(DELAY, ev -> {
    int h = height.getAndIncrement();
    if (h <= END_HEIGHT) {
      list.forEach(n -> {
        Object uo = makeUserObject(n, h);
        model.valueForPathChanged(new TreePath(n.getPath()), uo);
      });
    } else {
      ((Timer) ev.getSource()).stop();
    }
  }).start();
}

private static void startCollapseTimer(
    TreeExpansionEvent e, List<DefaultMutableTreeNode> list) {
  JTree tree = (JTree) e.getSource();
  TreePath path = e.getPath();
  TreeModel model = tree.getModel();
  AtomicInteger height = new AtomicInteger(END_HEIGHT);
  new Timer(DELAY, ev -> {
    int h = height.getAndDecrement();
    if (h >= START_HEIGHT) {
      list.forEach(n -> {
        Object uo = makeUserObject(n, h);
        model.valueForPathChanged(new TreePath(n.getPath()), uo);
      });
    } else {
      ((Timer) ev.getSource()).stop();
      tree.collapsePath(path);
    }
  }).start();
}
}}

----
- %%`Windows 10` + `Java 1.8.0_422`で作成した`example.jar`で`+`アイコンをクリックしてノード展開を実行するとアニメーションが開始されない%%
-- テスト中の古いソースコードで作成した`example.jar`が原因だった
-- `+`アイコンをクリックしてノード展開したかどうかではなく、ノードが選択状態かどうかをチェックするコードが残っていた

* 参考リンク [#reference]
- [[JTreeのノードを名前で検索して表示のフィルタリングを行う>Swing/TreeNodeFilter]]
- [[JTreeで親ノードが展開されたときに子ノードの選択状態を変更する>Swing/TreeSelectionPaths]]
- [[JTableで行の追加、削除アニメーション>Swing/SlideTableRows]]
- [[JTreeのノードを折り畳み不可に設定する>Swing/TreeNodeCollapseVeto]]
- [[JTreeのノード追加、削除>Swing/AddNode]]

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