布局上的动画更改

Chu*_*Tey 8 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);
        }
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这里提供了一个Gist可读性,并附带一个测试用例:https://gist.github.com/teyc/5668517

jew*_*sea 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();
Run Code Online (Sandbox Code Playgroud)

样品溶液输出

添加更多矩形并调整屏幕大小以强制新布局后更新动画师的输出(使用您在gist上提供的测试工具):

layoutanimator

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

在调试动画时,我发现如果你放慢动画速度会更容易.为了解决如何修复已发布的程序,我将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);
        }
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

关于使布局动画更改独立于用户TranslateX/Y设置

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

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

我从你的问题中分出了要点,以实现上面提到的自定义过渡增强.


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