【问题标题】:Animation upon layout changes布局更改时的动画
【发布时间】:2013-05-30 04:58:31
【问题描述】:

每次调整窗口大小时,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

【问题讨论】:

    标签: javafx-2


    【解决方案1】:

    这是一个非常巧妙的想法。我喜欢这个。你应该考虑将你的新布局管理器贡献给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 值不受影响。

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


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

    【讨论】:

    • 一般来说,是否可以保留 translateX/Y 的原始值?检查 Animation.STATUS 似乎没有帮助。我很想在它进入 jfxtras 之前把它清理干净。
    • 添加了“关于使布局动画更改独立表单用户 TranslateX/Y 设置”部分,以提供保留 translateX/Y 原始值的想法。
    • 在实现“使布局动画更改独立于用户 TranslateX/Y 设置”的解决方案中添加了 fork link
    • 我很赞同这件事的结果。谢谢。我已经用 TilePane 试过这个,它在那里也很好用。我将尝试使用标准应用程序菜单,看看效果如何。
    • 经过测试,使用不同的窗格,效果很好。真是令人印象深刻。
    猜你喜欢
    • 1970-01-01
    • 2020-11-08
    • 1970-01-01
    • 2018-11-06
    • 2013-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多