布局上的动画更改

时间:2013-05-30 04:58:31

标签: javafx-2

每次调整窗口大小时,JavaFX中的基本FlowPane都会列出内部的项目。但是,没有动画,结果相当刺耳。

我已经在FlowPane中的每个Node的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

1 个答案:

答案 0 :(得分:12)

这是一个非常巧妙的想法。我喜欢这个。您应该考虑将新的布局管理器贡献给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上提供的测试工具):

layoutanimator

关于调试动画和修复示例代码的其他问题

在调试动画时,我发现如果你减慢动画效果会更容易。为了解决如何修复已发布的程序,我将TranslateTransition设置为秒,让眼睛有足够的时间来查看实际情况。

减慢动画帮助我看到听众生成的实际动画似乎不会发生,直到节点重新启动后的一帧,导致节点暂时出现在其中的短暂故障& #39;目标位置,然后回到它的原始位置,然后慢慢地动画到目标位置。

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

您的代码使用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到节点的属性&#39; transform。然后,只有每个节点上的附加变换受到布局动画的内部工作的影响 - 用户的原始translateX / Y值不受影响。

I forked the gist from your question to implement the custom transition enhancement mentioned above


如果您热衷于此,可以查看openjfx code标签标题,TitledPanes和Accordions,并查看这些控件的外观如何处理其子节点的布局更改动画。