アニメーション付きレイアウト その 1

というわけで、JavaFX 勉強会用にサンプルを作りました。しかし、講演の中では時間も限られていたので、コードの説明はできませんでした。

そこで、この blog で解説をしようと思います。まずはじめは、レイアウトのサンプルです。

サンプルのコード/プロジェクトは
http://www.javainthebox.net/publication/20091211javafx.jp/samples/layout.html
よりダウンロード可能です。

JavaFX では、Java と異なりレイアウトマネージャのようにレイアウトだけ行うクラスはありません。その代わり、子ノードをレイアウトできるコンテナノードが用意されています。

レイアウト関連のクラスは javafx.scene.layout パッケージに定義されています。ベースになるクラスは Container クラスです。

レイアウトするクラスを自作する場合、Container クラスのサブクラスを作成してもいいのですが、もっといい方法があります。

その方法とは Panel クラスを使う方法です。

Swing だと JPanel クラスは汎用のコンテナに使えますが、JavaFX ではカスタムレイアウトを行なうことのできるコンテナという位置づけになります。

Panel クラスではプロパティの prefWidth、prefHeight、onLayout を定義します。どれも関数型のプロパティです。

prefWidth と prefHeight は Panel オブジェクトのサイズを返すための関数、つまり Swing 的にいえば getPreferredSize メソッドに相当します。

onLayout はレイアウトを行なう関数です。

つまりインスタンス化する時にこれらのプロパティを設定しておけば、Panel クラスのサブクラスを作成する必要はありません。

ただ、ここでは汎用に使えるコンテナクラスを作成しようと思っていたので、あえて Panel クラスのサブクラスにしました。

レイアウトの方法は、AWT の java.awt.FlowLayout クラスのパクリです。実際の FlowLayout クラスは左詰、右詰、センターとか、ベースラインで並べるなどの機能がありますが、すべて省略。

単に左詰にして、スペースがなくなれば次の列にレイアウトするというようにしました。

とはいっても、私自身まだレイアウトは極めていないので、よく分らないで書いている部分もあります。レイアウトに関するドキュメントは Amy Fowler が blog で書いているぐらいなので、いまいち分らないんですよね。

こういうこともソースが公開されていれば、分るのですが...

まぁ、それはさておき、AnimatedFlow クラスでは、前述した prefWidth、prefHeight、onLayout の 3 つのプロパティをオーバーライドしています。

prefWidth と prefHeight は引数の数値をそのまま返しているだけです。なぜ、この関数に引数があるのかもよく分りません。たとえば、Swing の getPreferredSize メソッドには引数がありません。

引数の意味があるはずなんですけど、API ドキュメントを読むだけだと、よく意味がわからないのです。

で、onLayout プロパティでレイアウトを行なっています。

    public override var onLayout = function(): Void {
        // 子要素をプリファードサイズにする
        resizeContent();
 
        // 横幅を決定する
        // width が設定されていれば、それを使用するが
        // 指定されていなければ、親の幅を利用する
        var w: Number;
        if (width > 0) {
            w = width;
        } else {
            var p: Parent = parent;
            while (p != null) {
                if (p instanceof Resizable) {
                    break;
                }
                p = p.parent;
            }
 
            if (p == null) {
                w = scene.width;
            } else {
                w = p.layoutBounds.width;
                if (w <= 0) {
                    w = scene.width;
                }
            }
        }
        var maxwidth = w - (marginLeft + marginRight + hgap*2);
         
        var x = 0.0;
        var y = marginTop + vgap;
        var rowh = 0.0;
        var start = 0;
 
        for (node in getManaged(content)) {
            if (node.visible) {
                // 水平方向にいくつノードをレイアウトできるか調べ、
                // 一列分ノードを移動する
                if (<span style="color: red;">(x == 0) or ((x + node.layoutBounds.width) <= maxwidth)</span>) {
                    // 一列の最後になるまで、x に node の幅と水平方向のギャップを加えていく
                    if (x > 0) {
                        x += hgap;
                    }
                    x += node.layoutBounds.width;
                    rowh = Math.max(rowh, node.layoutBounds.height);
                } else {
                    // 一列に収まらなくなったら、それまでのノードを移動し、y 座標を更新する
                    rowh = moveNodes(marginLeft + hgap, y, rowh, start, indexof node);
                    x = node.layoutBounds.width;
                    y += vgap + rowh;
                    rowh = node.layoutBounds.height;
                    start = indexof node;
                }
            }
        }
        // 最後の列の移動
        moveNodes(marginLeft + hgap, y, rowh, start, sizeof content);
    }

x が水平方向の座標になります。赤字の部分で node が一列の最後になるまで、x に node の幅と水平方向のギャップを加えていきます。

x == 0 で比較しているのは、幅が node の幅より小さくても、1 つだけは水平方向に配置させるためです。それをしておかないと、無限ループになってしまいます。

一列に収まらなくなったら、moveNodes 関数をコールして、そこまでのノードを移動させます。そして、x と y を更新します。

最後の moveNodes は最後の行のノードの移動を行なうためです。

次に、moveNodes を示します。

    function moveNodes(x: Number, y: Number, height: Number,
                        rowStart: Integer, rowEnd: Integer): Number {
        var xx = x;
 
        // 一列分のノードの移動位置を計算し、移動させる
	for (i in [rowStart..rowEnd]) {
            var node = getManaged(content)[i];
            if (node.visible) {
                var cy = y + (height - node.layoutBounds.height) / 2;
                moveNode(node, xx, cy);
                xx += node.layoutBounds.width + hgap;
	    }
	}
        
        return height;
    }

この関数は 1 つずつノードを取りだして、座標を計算し、moveNode 関数をコールして移動させています。

    function moveNode(node: Node, x: Number, y: Number): Void {
        def target = node;
        var preX = target.layoutX;
        var preY = target.layoutY;
 
        // ノードの移動
        Timeline {
            keyFrames: [
                KeyFrame {
                    time: 0s
                    values: [target.layoutX => preX, target.layoutY => preY]
                },
                KeyFrame {
                    time: 400ms
                    values: [target.layoutX => x, target.layoutY => y]
                },
            ]
        }.play();
    }

最後の moveNode 関数では Timeline クラスを使って、移動を行なうアニメーションを行ないます。ここでは Timeline クラスを直接使いましたが、javafx.animation.transition.TranslateTransition クラスを使うのでも、まったく問題ありません。

レイアウトを行なう場合、座標には translateX や translateY を使うのではなく、layoutX と layoutY を使用します。

ここでは 1 つずつノードを移動させていますが、javafx.animation.transition.ParallelTransition クラスを使ってまとめて移動させるのでもかまいません。

しかし、JavaFX ではアニメーションで使用するスレッドを最適化しているので、ここでの記述のように複数の Timeline オブジェクトを使用してアニメーションさせても、使用するスレッドは増加しません。

なので、あえて ParallelTransition クラスは使いませんでした。

マウスオーバーすると、ノードが拡大する部分については次回。