12

JavaFX の基本的な FlowPane は、ウィンドウのサイズが変更されるたびに項目をレイアウトします。ただし、アニメーションはなく、結果はかなり耳障りです。

FlowPane 内の各ノードの layoutX および layoutY プロパティに変更リスナーを接続した結果、多かれ少なかれ機能しますが、ウィンドウのサイズをすばやく変更すると、要素が一貫性のない場所に残ることがあります。

私は何が欠けていますか?

package javafxapplication1;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.animation.Transition;
import javafx.animation.TranslateTransition;
import javafx.beans.property.DoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.util.Duration;

/**
 * Animates an object when its position is changed. For instance, when
 * additional items are added to a Region, and the layout has changed, then the
 * layout animator makes the transition by sliding each item into its final
 * place.
 */
public class LayoutAnimator implements ChangeListener, ListChangeListener<Node> {

  private Map<Node, Transition> nodesInTransition;

  public LayoutAnimator() {
    this.nodesInTransition = new HashMap<>();
  }

  /**
   * Animates all the children of a Region.
   * <code>
   *   VBox myVbox = new VBox();
   *   LayoutAnimator animator = new LayoutAnimator();
   *   animator.observe(myVbox.getChildren());
   * </code>
   *
   * @param nodes
   */
  public void observe(ObservableList<Node> nodes) {
    for (Node node : nodes) {
      this.observe(node);
    }
    nodes.addListener(this);
  }

  public void unobserve(ObservableList<Node> nodes) {
    nodes.removeListener(this);
  }

  public void observe(Node n) {
    n.layoutXProperty().addListener(this);
    n.layoutYProperty().addListener(this);
  }

  public void unobserve(Node n) {
    n.layoutXProperty().removeListener(this);
    n.layoutYProperty().removeListener(this);
  }

  @Override
  public void changed(ObservableValue ov, Object oldValue, Object newValue) {
    final Double oldValueDouble = (Double) oldValue;
    final Double newValueDouble = (Double) newValue;
    final Double changeValueDouble = newValueDouble - oldValueDouble;
    DoubleProperty doubleProperty = (DoubleProperty) ov;

    Node node = (Node) doubleProperty.getBean();
    final TranslateTransition t;
    if ((TranslateTransition) nodesInTransition.get(node) == null) {
      t = new TranslateTransition(Duration.millis(150), node);
    } else {
      t = (TranslateTransition) nodesInTransition.get(node);
    }

    if (doubleProperty.getName().equals("layoutX")) {
      Double orig = node.getTranslateX();
      if (Double.compare(t.getFromX(), Double.NaN) == 0) {
        t.setFromX(orig - changeValueDouble);
        t.setToX(orig);
      }
    }
    if (doubleProperty.getName().equals("layoutY")) {
      Double orig = node.getTranslateY();
      if (Double.compare(t.getFromY(), Double.NaN) == 0) {
        t.setFromY(orig - changeValueDouble);
        t.setToY(orig);
      }
    }
    t.play();

  }

  @Override
  public void onChanged(ListChangeListener.Change change) {
    while (change.next()) {
      if (change.wasAdded()) {
        for (Node node : (List<Node>) change.getAddedSubList()) {
          this.observe(node);
        }
      } else if (change.wasRemoved()) {
        for (Node node : (List<Node>) change.getRemoved()) {
          this.unobserve(node);
        }
      }
    }
  }
}

Gist は読みやすくするためにここで利用でき、テスト ケースが付属しています: https://gist.github.com/teyc/5668517

4

1 に答える 1

16

これは本当に素晴らしいアイデアです。私はこれが好き。新しいレイアウト マネージャーをjfxtrasに提供することを検討する必要があります。

最初に投稿したソリューションが機能しない理由

問題は、translateX/Y 値の元の値を記録しようとし、その元の値で変換トランジションを終了しようとするロジックです。

TranslateTransition が進行中の場合、translateX/Y 値が変更されます。現在のコードでは、アニメーションが終了する前に画面のサイズをすばやく変更する場合、TranslateTransition の toX/Y プロパティを現在の translateX/Y 値に設定します。これにより、アニメーションの最終的な静止位置が、ノードの目的の最終的な静止位置ではなく、中間点の前になります (目的の位置は、ノードの移動 X/Y 値が 0 であるポイントにすぎません)。

修正方法

修正は簡単です。TranslateTransition の toX/Y は常に 0 である必要があるため、トランジションは常に、現在のレイアウト位置がトランスレート デルタなしで想定されているものにノードをトランスレートすることになります。

サンプル ソリューション コード スニペット

コア更新された移行コードは次のとおりです。

TranslateTransition t;
switch (doubleProperty.getName()) {
  case  "layoutX":
    t = nodeXTransitions.get(node);
    if (t == null) {
      t = new TranslateTransition(Duration.millis(150), node);
      t.setToX(0);
      nodeXTransitions.put(node, t);
    }
    t.setFromX(node.getTranslateX() - delta);
    node.setTranslateX(node.getTranslateX() - delta);
    break;

  default: // "layoutY"
    t = nodeYTransitions.get(node);
    if (t == null) {
      t = new TranslateTransition(Duration.millis(150), node);
      t.setToY(0);
      nodeYTransitions.put(node, t);
    }
    t.setFromY(node.getTranslateY() - delta);
    node.setTranslateY(node.getTranslateY() - delta);
}

t.playFromStart();

サンプル ソリューションの出力

いくつかの長方形を追加し、画面のサイズを変更して新しいレイアウトを強制した後の更新されたアニメーターの出力は次のとおりです (gist で提供したテスト ハーネスを使用):

レイアウトアニメーター

アニメーションのデバッグとサンプル コードのその他の問題の修正について

アニメーションのデバッグでは、アニメーションの速度を落とした方が簡単だと思います。投稿されたプログラムを修正する方法を考え出すために、TranslateTransition を 1 秒に設定して、実際に何が起こっているかを確認するのに十分な時間を与えます。

アニメーションを遅くすると、ノードが再配置された後のフレームまでリスナーによって生成された実際のアニメーションが発生しないように見え、ノードがターゲット位置に一時的に表示される短い不具合が発生することがわかりました。元の位置に戻ってから、ゆっくりと目標の位置に移動します。

初期位置の不具合の修正は、アニメーションの再生を開始する前に、リスナーのノードを元の位置に戻すことです。

コードは nodesInTransition マップを使用して現在遷移中のノードを追跡しますが、マップには何も配置しません。nodeXTransitions と nodeYTransitions に名前を変更しました (別々のものを使用する方が簡単に思えたので、単一の遷移ではなく、別々の x と y 遷移を使用するため)。新しいトランジションを作成するときに古いトランジションを停止できるように、ノード トランジションを作成時にマップに配置します。マップ ロジックがなくてもすべてが正常に機能するように見えたため (おそらく、JavaFX は既にアニメーション フレームワーク内で暗黙的にこのようなことを行っているようです)、これは厳密には必要ではないように思われましたが、安全に実行できるように思われるので、そのままにしました。

上記の変更を行った後、すべてが正常に機能するように見えました。

さらなる改善の可能性

アニメーションのタイミングを改善することで、アニメーションが部分的に再生されて再レイアウトが発生した場合、新しい再レイアウトのために最初からアニメーション全体を再生したくない場合があります。または、アニメートされたすべてのノードを一定の時間ではなく、一定の同じ速度で移動させたい場合があります。でも視覚的にはあまり気にならなかったので、あまり気にしませんでした。

テストシステム

Java8b91 と OS X 10.8 でテストを行いました。

更新されたソリューションの完全なコード

更新されたレイアウト アニメーターの完全なコードは次のとおりです。

import javafx.animation.TranslateTransition;
import javafx.beans.property.DoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.util.Duration;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Animates an object when its position is changed. For instance, when
 * additional items are added to a Region, and the layout has changed, then the
 * layout animator makes the transition by sliding each item into its final
 * place.
 */
public class LayoutAnimator implements ChangeListener<Number>, ListChangeListener<Node> {

  private Map<Node, TranslateTransition> nodeXTransitions = new HashMap<>();
  private Map<Node, TranslateTransition> nodeYTransitions = new HashMap<>();

  /**
   * Animates all the children of a Region.
   * <code>
   *   VBox myVbox = new VBox();
   *   LayoutAnimator animator = new LayoutAnimator();
   *   animator.observe(myVbox.getChildren());
   * </code>
   *
   * @param nodes
   */
  public void observe(ObservableList<Node> nodes) {
    for (Node node : nodes) {
      this.observe(node);
    }
    nodes.addListener(this);
  }

  public void unobserve(ObservableList<Node> nodes) {
    nodes.removeListener(this);
  }

  public void observe(Node n) {
    n.layoutXProperty().addListener(this);
    n.layoutYProperty().addListener(this);
  }

  public void unobserve(Node n) {
    n.layoutXProperty().removeListener(this);
    n.layoutYProperty().removeListener(this);
  }

  @Override
  public void changed(ObservableValue<? extends Number> ov, Number oldValue, Number newValue) {
    final double delta = newValue.doubleValue() - oldValue.doubleValue();
    final DoubleProperty doubleProperty = (DoubleProperty) ov;
    final Node node = (Node) doubleProperty.getBean();

    TranslateTransition t;
    switch (doubleProperty.getName()) {
      case  "layoutX":
        t = nodeXTransitions.get(node);
        if (t == null) {
          t = new TranslateTransition(Duration.millis(150), node);
          t.setToX(0);
          nodeXTransitions.put(node, t);
        }
        t.setFromX(node.getTranslateX() - delta);
        node.setTranslateX(node.getTranslateX() - delta);
        break;

      default: // "layoutY"
        t = nodeYTransitions.get(node);
        if (t == null) {
          t = new TranslateTransition(Duration.millis(150), node);
          t.setToY(0);
          nodeYTransitions.put(node, t);
        }
        t.setFromY(node.getTranslateY() - delta);
        node.setTranslateY(node.getTranslateY() - delta);
    }

    t.playFromStart();
  }

  @Override
  public void onChanged(Change change) {
    while (change.next()) {
      if (change.wasAdded()) {
        for (Node node : (List<Node>) change.getAddedSubList()) {
          this.observe(node);
        }
      } else if (change.wasRemoved()) {
        for (Node node : (List<Node>) change.getRemoved()) {
          this.unobserve(node);
        }
      }
    }
  }
}

ユーザーの TranslateX/Y 設定とは独立したレイアウト アニメーションの変更について

現在提示されているソリューションの欠点の 1 つは、カスタム レイアウト マネージャーのユーザーが translateX/Y 設定をレイアウトされたノードに直接適用した場合、レイアウト マネージャーがすべてのコンテンツを中継するときにそれらの値が失われることです (translateX として)。 /Y は最終的に 0 に戻されます)。

ユーザーの translateX/Y を維持するために、ソリューションを更新して、トランジションを翻訳するのではなく、カスタム トランジションを使用することができます。カスタム トランジションは、 Translate to ノードのtransformのプロパティに適用できます。次に、各ノードの追加の変換のみが、レイアウト アニメーションの内部動作によって影響を受けます。ユーザーの元の translateX/Y 値は影響を受けません。

上記のカスタムトランジション拡張を実装するために、あなたの質問から要点をフォークしました


興味があれば、タブ ヘッダー、TitledPanes、および Accordions のopenjfx コードを調べて、これらのコントロールのスキンが子ノードのレイアウト変更のアニメーションをどのように処理するかを確認できます。

于 2013-05-30T08:23:11.020 に答える