2013-05-30 53 views
8

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

要旨可這裏的可讀性,並帶有一個測試用例:https://gist.github.com/teyc/5668517

回答

10

這是一個非常不錯的主意。我喜歡這個。你應該考慮提供你的新佈局管理器到jfxtras

爲什麼你最初發布解決方案無效

你的問題是各地要記錄的平移X/Y值的初始值和結束你的翻譯在那個原始值轉換的邏輯。

當TranslateTransition正在進行時,它將修改translateX/Y值。使用當前的代碼,如果在動畫完成之前快速調整屏幕大小,則可以將TranslateTransition的toX/Y屬性設置爲當前的translateX/Y值。這使得動畫的最終安放位置處於某個先前的中間點,而不是節點的期望最終安放位置(所需位置僅是節點的translateX/Y值爲0的點)。

如何解決它

解決方法是簡單的 - TOX/Y爲TranslateTransition應該永遠是零,所以過渡最終總是翻譯成任何它目前的佈局位置應該是與節點沒有翻譯三角洲。

樣品溶液代碼段

這裏的核心是更新的轉換代碼:

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

樣品溶液輸出

增加了更多的矩形後更新動畫的產量和調整屏幕強制新佈局是(使用您提供的測試工具):

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

來自於用戶的平移X製作佈局動畫的變化獨立/ Y設置

一目前呈現的解決方案的缺點是,如果自定義佈局管理器的用戶直接應用了translateX/Y設置t o佈局節點,然後當佈局管理器中繼所有內容時(因爲translateX/Y最終設置爲0),它們將丟失這些值。

爲了保留用戶的translateX/Y,解決方案可以更新爲使用自定義轉換而不是轉換轉換。自定義轉換可應用於節點'transformTranslate的屬性。然後,只有佈局動畫的內部工作纔會影響每個節點上的附加轉換 - 用戶的原始translateX/Y值不受影響。

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


如果你是敏銳,你可以看看在openjfx code的標籤頭,TitledPanes和手風琴,看看這些控件的外觀如何處理的佈局改變動畫爲他們的孩子節點。

+0

一般性,是否可以保留translateX/Y的原始值?檢查Animation.STATUS似乎沒有幫助。我很想在進入jfxtras之前清理它。 –

+0

添加了「關於使佈局動畫更改獨立於用戶TranslateX/Y設置」部分,以提供保留translateX/Y原始值的想法。 – jewelsea

+0

在解決方案中添加了[fork link](https://gist.github.com/jewelsea/5683558),實現了「使佈局動畫更改與用戶TranslateX/Y設置無關」。 – jewelsea