概要

JTreeのノード展開、折り畳みでその子ノードの高さを増減するアニメーションを実行します。

サンプルコード

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;
  }
}
View in GitHub: Java, Kotlin

解説

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

参考リンク

コメント