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);
        }
      }
    }
  }
}

此处提供了一个要点以提高可读性,并附带一个测试用例:https ://gist.github.com/teyc/5668517

4

1 回答 1

16

这是一个非常巧妙的想法。我喜欢这个。您应该考虑将您的新布局管理器贡献给jfxtras

为什么您最初发布的解决方案不起作用

您的问题是围绕尝试记录 translateX/Y 值的原始值并以该原始值结束翻译转换的逻辑。

当 TranslateTransition 正在进行时,它会修改 translateX/Y 值。使用当前代码,如果您在动画完成之前快速调整屏幕大小,则将 TranslateTransition 的 toX/Y 属性设置为当前 translateX/Y 值。这使动画的最终静止位置成为某个先前的中间点,而不是节点所需的最终静止位置(所需位置只是节点的 translateX/Y 值为 0 的点)。

如何修复它

修复很简单 - TranslateTransition 的 toX/Y 应该始终为零,因此转换总是最终将节点转换到它的当前布局位置应该是没有转换增量的任何位置。

示例解决方案代码片段

这是核心更新的过渡代码:

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 设置为一秒,以便让眼睛有足够的时间来查看实际发生的情况。

放慢动画速度帮助我看到听众生成的实际动画似乎直到节点被重新布置后的一帧才发生,在节点将暂时出现在其目标位置的地方出现短暂的故障,然后去回到原来的位置,然后慢慢动画到目标位置。

初始定位故障的修复是在开始播放动画之前将侦听器中的节点转换回原始位置。

您的代码使用 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 设置

当前提出的解决方案的一个缺点是,如果自定义布局管理器的用户已将 translateX/Y 设置直接应用于布局节点,那么当布局管理器中继所有内容时,它们将丢失这些值(因为 translateX /Y 最终被设置回 0)。

为了保留用户的 translateX/Y,可以更新解决方案以使用自定义转换而不是转换转换。自定义转换可以应用于Translate的属性到节点的transform。然后只有每个节点上的附加变换会受到布局动画的内部工作的影响——用户的原始 translateX/Y 值不受影响。

我从您的问题中提取了要点以实现上面提到的自定义过渡增强


如果您有兴趣,可以查看选项卡标题、TitledPanes 和 Accordions 的 openjfx代码,并了解这些控件的外观如何处理其子节点的布局更改动画。

于 2013-05-30T08:23:11.020 回答