[JavaFX] Dynamical layouts in FX

原文はこちら。
https://blogs.oracle.com/jfxprg/entry/dynamical_layouts_in_fx

JavaFXのレイアウトコードを引き継いでから、私は、動的レイアウト(Dynamical Layouts)に関連するバグ報告があり、多くの人々が実装に苦労されていることに気付きました。動的レイアウトとは、レイアウトPane(layout Pane)が子のサイズ変更や位置を決めるだけでなく、取得したサイズによっては子のリストを操作するものです。

標準Layout Paneにおける典型的な流れは以下の通りです。
  1. 親ペインがprefWidth/prefHeightを呼び出す。preferred sizeは現在の子とPaneプロパティや制約の組み合わせから計算される。
  2. 親が実際の幅や高さを割り当てる
  3. レイアウトを幅や高さ、手順1で使ったものと同じ子のセットで計算する
Dynamical Paneは以下のような流れになっています。
  1. preferred sizeは、固定もしくはPaneプロパティもしくは(子を作成するベースとなる)アイテムによって変化する
  2. 親は実際の幅や高さを割り当てる
  3. レイアウトを幅や高さから計算し、それに応じて子を作成する
固定サイズの長方形を有する簡単な動的Paneは以下のように実装できます。
private class GrowingPane extends Pane {

        private static final int PREF_NUMBER = 2;
        private static final int RECTANGLE_SIZE = 50;
        private static final int PADDING = 10;

        @Override
        protected double computePrefWidth(double height) {
            return PREF_NUMBER * RECTANGLE_SIZE + (2 + PREF_NUMBER - 1) * PADDING;
        }

        @Override
        protected double computePrefHeight(double width) {
            return RECTANGLE_SIZE + 2 * PADDING;
        }


        @Override
        protected void layoutChildren() {
            final double w = getWidth();

            getChildren().clear();
            // count the number of Rectangles that will fit
            final int num = (int) (w - PADDING) / (RECTANGLE_SIZE + PADDING);

            int curX = PADDING;
            // Do the layout
            for (int i = 0; i < num; ++i) {
                Rectangle rec = new Rectangle(RECTANGLE_SIZE, RECTANGLE_SIZE, Color.LIGHTGREEN);
                getChildren().add(rec);
                rec.relocate(curX, PADDING);
                curX += RECTANGLE_SIZE + PADDING;
            }

        }
    }

今度はもっと洗練されたバージョン、固定サイズではないボタンを持つ場合を見てみましょう。このサンプルのために、Buttonテキストを生成しています。後続の各ボタンのテキストは倍のサイズになっています。computePrefWidth/computePrefHeightの違いに注意してください。
private class GrowingPane2 extends Pane {

        private static final int PREF_NUMBER = 2;
        private static final String BASE_STRING = "A";
        private double prefWidth = -1;
        private double prefHeight = -1;
        private static final int PADDING = 10;

        @Override
        protected double computePrefWidth(double height) {
            if (prefWidth == -1) {
                computePrefSize();
            }
            return prefWidth;
        }

        private void computePrefSize() {
            String str = BASE_STRING;

            prefHeight = snapSpace(2 * PADDING + computeHeight());

            prefWidth = PADDING;
            for (int i = 0; i < PREF_NUMBER; ++i) {
                prefWidth += computeWidthForString(str) + PADDING;
                str += str;
            }
            prefWidth = snapSpace(prefWidth);

        }

        private double computeHeight() {
            Button b = new Button(BASE_STRING);
            getChildren().add(b);
            b.applyCss();
            double result = b.prefHeight(-1);
            getChildren().remove(b);
            return result;
        }

        private double computeWidthForString(String str) {
            Button b = new Button(str);
            getChildren().add(b);
            b.applyCss();
            double result = b.prefWidth(-1);
            getChildren().remove(b);
            return result;
        }

        @Override
        protected double computePrefHeight(double width) {
            if (prefHeight == -1) {
                computePrefSize();
            }
            return prefHeight;
        }


        @Override
        protected void layoutChildren() {
            final double w = getWidth();

            getChildren().clear();

            double widthLeft = w - PADDING;
            double curX = PADDING;
            String s = BASE_STRING;

            while(widthLeft >= PADDING) {
                double bw = computeWidthForString(s);
                if (bw + PADDING > widthLeft) {
                    break;
                }
                Button b = new Button(s);
                getChildren().add(b);
                b.applyCss();
                s += s;

                b.autosize();
                b.relocate(curX, PADDING);
                curX += bw + PADDING;
                widthLeft -= bw + PADDING;
            }

        }
    }

現実には、ボタンやその他諸々をアイテムリストや他のモデルを基にして生成し、このモデルの変化によって計算されたサイズを無効にしたいということがあるかと思います。

計算を見てみると、preferred sizeを計算する前に、子の追加や削除、 applyCss() を呼び出していたことにお気付きかもしれません。これは、Layout Passの後にCSS Passを実行するため、Layout Passで子を変更するためには、すべての新しいノードに対し、明示的にCSS Passを実行する必要があります。CSSは、コンテキスト(シーングラフにおける位置)に依存しますので、まず最初に子を追加する必要があります。もちろん、絶対的に正確であるために、子を正しい位置に追加し、計算終了時にリストをクリアする必要があります。

0 件のコメント:

コメントを投稿