布局更改时的动画 [英] Animation upon layout changes

查看:52
本文介绍了布局更改时的动画的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

解决方案

这是一个非常简洁的想法.我喜欢这个.您应该考虑将新的布局管理器贡献给 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设置进行版式动画更改

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

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

我从您的问题中摘录了要实现上述自定义转换增强功能的要点.


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

The basic FlowPane in JavaFX lays out the items inside each time the window is resized. However, there is no animation and the result is rather jarring.

I've hooked up a change listener on the layoutX and layoutY properties of each Node inside the FlowPane and the result more or less works, but sometimes when I resize the window quickly, the elements are left in inconsistent places.

What am I missing?

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

A Gist is available here for readability and comes with a test case: https://gist.github.com/teyc/5668517

解决方案

This is a really neat idea. I like this. You should consider contributing your new layout manager to jfxtras.

Why your Originally Posted Solution Does not Work

Your problem is the logic around trying to record an original value for the translateX/Y values and ending your translate transition at that original value.

When a TranslateTransition is in progress, it modifies the translateX/Y values. With your current code, if you resize the screen rapidly before your animations finish, you set the toX/Y properties of your TranslateTransition to the current translateX/Y values. This makes the final resting place of the animation some prior intermediate point rather than the desired final resting place for the node (the desired place is just the point where the translateX/Y values for the node are 0).

How to Fix it

The fix is simple - toX/Y for the TranslateTransition should always be zero, so the transition always ends up translating the node to whatever it's current layout position is supposed to be with no translate delta.

Sample Solution Code Snippet

Here is the core updated transition code:

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

Sample Solution Output

Output of the updated animator after adding a few more rectangles and resizing the screen to force new layouts is (using the test harness you supplied on gist):

On Debugging Animations and Fixing Additional Issues with your Sample Code

In debugging animations, I find it is easier if you slow down animations. To work out how to fix the posted program, I set the TranslateTransition to a second to give the eye enough time to see what is actually going on.

Slowing down the animation helped me see that the actual animations generated by your listeners don't seem to take place until a frame after the node has been relaid out, making a brief glitch where the node will momentarily appear in it's target position, then go back to it's original position, then slowly animate to the target position.

The fix for the initial positioning glitch is to translate the node in the listener back to original position before starting to play the animation.

Your code makes use of a nodesInTransition map to keep track of the currently transitioning nodes, but it never puts anything in the map. I renamed it to nodeXTransitions and nodeYTransitions (because I use separate x and y transitions rather than a single transition for both because using separate ones seemed easier). I place the node transitions into the map as they are created so that I can stop old transitions when I create new ones. This didn't seem strictly necessary as everything seemed to work fine without the map logic (perhaps JavaFX already does something like this implicitly inside it's animation framework), but it seems like a safe thing to do, so I kept it.

After I made the changes detailed above everything seemed to work OK.

Potential Further Improvements

There are perhaps some improvements which could be made for timing of animations so that if an animation is partially played and a relayout occurs, maybe you don't want to play a whole animation from the beginning for the new relayout. Or perhaps you might want to have all of the animated nodes move at a constant, identical velocity rather than for a duration. But visually, that didn't seem to matter much, so I wouldn't really worry about it.

Test System

I did my testing on Java8b91 and OS X 10.8.

Full Code for the Updated Solution

Full code the updated layout animator is:

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

On Making Layout Animation Changes Independent from User TranslateX/Y Settings

One drawback of the solution as currently presented is that if the user of the custom layout manager had applied translateX/Y settings directly to the laid out nodes, then they will lose those values as the layout manager relays out all the content (as the translateX/Y ends up being set back to 0).

To preserve the user's translateX/Y, the solution could be updated to use custom transitions rather than translate transitions. The custom transitions can be applied to properties of a Translate to the nodes' transform. Then only the additional transform on each node is impacted by the inner workings of the layout animation - the user's original translateX/Y values are not affected.

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


If you were keen, you could look over the openjfx code for tab headers, TitledPanes and Accordions and see how the skins for those controls handle the animation of layout changes for their child nodes.

这篇关于布局更改时的动画的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆