动画依赖于之前的动画

Animation depends on previous animations

我正在尝试设置节点动画,这取决于该节点以及其他节点之前的动画。
为了演示这个问题,我将使用一个简单的 Pane 和 4 Label children:

主要class以及模型和视图class是:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch; 
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;

public final class Puzzle extends Application{

    private Controller controller;
    @Override
    public void start(Stage stage) throws Exception {
        controller = new Controller();
        BorderPane root = new BorderPane(controller.getBoardPane());
        root.setTop(controller.getControlPane());
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) { launch();}
}

class View{

    private static final double size = 70;
    private static Duration animationDuration = Duration.millis(600);
    private int[][] cellModels;
    private Node[][] cellNodes;
    private CountDownLatch latch;
    Button play = new Button("Play");
    private Pane board, control = new HBox(play);;

    View(Model model) {
        cellModels = model.getCellModels();
        cellNodes = new Node[cellModels.length][cellModels[0].length];
        makeBoardPane();
        ((HBox) control).setAlignment(Pos.CENTER_RIGHT);
    }

    private void makeBoardPane() {

        board = new Pane();
        for (int row = 0; row < cellModels.length ; row ++ ) {
            for (int col = 0; col < cellModels[row].length ; col ++ ) {

               Label label = new Label(String.valueOf(cellModels[row][col]));
               label.setPrefSize(size, size);
               Point2D location = getLocationByRowCol(row, col);
               label.setLayoutX(location.getX());
               label.setLayoutY(location.getY());
               label.setStyle("-fx-border-color:blue");
               label.setAlignment(Pos.CENTER);
               cellNodes[row][col] = label;
               board.getChildren().add(label);
            }
        }
    }

    synchronized void updateCell(int id, int row, int column) {

        if(latch !=null) {
            try {
                latch.await();
            } catch (InterruptedException ex) { ex.printStackTrace();}
        }
        latch = new CountDownLatch(1);

        Node node = getNodesById(id).get(0);
        Point2D newLocation = getLocationByRowCol(row, column);
        Point2D moveNodeTo = node.parentToLocal(newLocation );

        TranslateTransition transition = new TranslateTransition(animationDuration, node);
        transition.setFromX(0); transition.setFromY(0);
        transition.setToX(moveNodeTo.getX());
        transition.setToY(moveNodeTo.getY());

        //set animated node layout to the translation co-ordinates:
        //
        transition.setOnFinished(ae -> {
            node.setLayoutX(node.getLayoutX() + node.getTranslateX());
            node.setLayoutY(node.getLayoutY() + node.getTranslateY());
            node.setTranslateX(0);
            node.setTranslateY(0);
            latch.countDown();
        });
        transition.play();
    }

    private List<Node> getNodesById(int...ids) {

        List<Node> nodes = new ArrayList<>();
        for(Node node : board.getChildren()) {
            if(!(node instanceof Label)) { continue; }
            for(int id : ids) {
                if(((Label)node).getText().equals(String.valueOf(id))) {
                    nodes.add(node);
                    break;
                }
            }
        }
        return nodes ;
    }

    private Point2D getLocationByRowCol(int row, int col) {
        return new Point2D(size * col, size * row);
    }

    Pane getBoardPane() { return board; }
    Pane getControlPane() { return control;}
    Button getPlayBtn() {return play ;}
}

class Model{

    private int[][] cellModels = new int[][] { {0,1}, {2,3} };
    private SimpleObjectProperty<int[][]> cellModelsProperty =
            cellModelsProperty = new SimpleObjectProperty<>(cellModels);

    void addChangeListener(ChangeListener<int[][]> listener) {
        cellModelsProperty.addListener(listener);
    }

    int[][] getCellModels() {
        return  (cellModelsProperty == null) ? null : cellModelsProperty.get();
    }

    void setCellModels(int[][] cellModels) {
        cellModelsProperty.set(cellModels);
    }
}

和控制器class:

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Pane;

class Controller {

    private View view ;
    private Model model;


    Controller() {
        model = new Model();
        model.addChangeListener(getModelCangeListener());//observe model changes
        view = new View(model);
        view.getPlayBtn().setOnAction( a -> shuffle());  //animation works fine
        //view.getPlayBtn().setOnAction( a -> IntStream.
        //        range(0,4).forEach( (i)-> shuffle())); //messes the animation
    }

    private ChangeListener<int[][]> getModelCangeListener() {

        return  (ObservableValue<? extends int[][]> observable,
                int[][] oldValue, int[][] newValue)-> {
                    for (int row = 0; row < newValue.length ; row++) {
                        for (int col = 0; col < newValue[row].length ; col++) {
                            if(newValue[row][col] != oldValue[row][col]) {
                                final int fRow = row, fCol = col;
                                new Thread( () -> view.updateCell(
                                    newValue[fRow][fCol], fRow, fCol)).start();
                            }
                        }
                    }
                };
    }

    void shuffle() {
        int[][] modelData = model.getCellModels();
        int rows = modelData.length, columns = modelData[0].length;
        int[][] newModelData = new int[rows][columns];
        for (int row = 0; row < rows ; row ++) {
            for (int col = 0; col < columns ; col ++) {
                int colIndex = ((col+1) < columns ) ? col + 1  : 0;
                int rowIndex =  ((col+1) < columns ) ?  row : ((row + 1) < rows) ? row +1 : 0;
                newModelData[row][col] = modelData[rowIndex][colIndex];
            }
        }

        model.setCellModels(newModelData);
    }

    Pane getBoardPane() { return view.getBoardPane(); }

    Pane getControlPane() { return view.getControlPane(); }
}

播放按钮处理程序更改模型数据(参见 shuffle()),更改触发 ChangeListenerChangeListener 通过在单独的线程上调用 view.updateCell(..) 为每个更改的标签设置动画。这一切都按预期工作。
当我尝试 运行 几个连续的模型更新 (shuffle()) 时,问题就开始了。为了模拟它,我改变了

view.getPlayBtn().setOnAction( a -> shuffle()); 

view.getPlayBtn().setOnAction( a -> IntStream.range(0,4).forEach( (i)-> shuffle()));

这会弄乱动画(播放顺序错误并以错误的位置结束)。
这确实 而不是 令人惊讶:要正常工作,动画必须按特定顺序播放:标签应该 re-animated 只有在所有四个标签都完成了它们之前的动画之后。
代码 posted 运行s 每次在线程上更新,因此无法保证执行顺序。

我的问题是实现所需的多个节点和多个动画序列的正确方法是什么?

我查看了 SequentialTransition,但我无法弄清楚如何使用它来解决所讨论的问题。
我确实想出了一个解决方案,我将 post 作为答案,因为这个 post 的长度,而且我认为这个解决方案不好。

我提出的解决方案涉及控制更新线程的执行顺序。
为此,我通过实现基于 this 的内部 class 更改了控制器 class 所以回答。请参阅以下代码中的 UpdateView
我还修改了 getModelCangeListener() 以使用 UpdateView:

import java.util.stream.IntStream; 
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Pane;

class Controller {   
    private View view ;
    private Model model;
    private static int threadNumber = 0, threadAllowedToRun = 0;
    private static final Object myLock = new Object();

    Controller() {
        model = new Model();
        model.addChangeListener(getModelCangeListener());//observe model changes
        view = new View(model);
        view.getPlayBtn().setOnAction( a -> IntStream.
                                          range(0,4).forEach( (i)-> shuffle())); 
    }

    private ChangeListener<int[][]> getModelCangeListener() {

        return  (ObservableValue<? extends int[][]> observable,
                int[][] oldValue, int[][] newValue)-> {
                    for (int row = 0; row < newValue.length ; row++) {
                        for (int col = 0; col < newValue[row].length ; col++) {
                            if(newValue[row][col] != oldValue[row][col]) {
                                final int fRow = row, fCol = col;
                                new Thread( new UpdateView(
                                        newValue[fRow][fCol], fRow, fCol)).start();
                            }
                        }
                    }
                };
    }

    private void shuffle() {
        int[][] modelData = model.getCellModels();
        int rows = modelData.length, columns = modelData[0].length;
        int[][] newModelData = new int[rows][columns];
        for (int row = 0; row < rows ; row ++) {
            for (int col = 0; col < columns ; col ++) {
                int colIndex = ((col+1) < columns ) ? col + 1  : 0;
                int rowIndex =  ((col+1) < columns ) ?  row : ((row + 1) < rows) ? row +1 : 0;
                newModelData[row][col] = modelData[rowIndex][colIndex];
            }
        }

        model.setCellModels(newModelData);
    }

    Pane getBoardPane() { return view.getBoardPane(); }

    Pane getControlPane() { return view.getControlPane(); }

    //
    class UpdateView implements Runnable {

        private int id, row, col, threadID;

        UpdateView(int id, int row, int col) {
            this.id = id;  this.row = row;  this.col = col;
            threadID = threadNumber++;
        }

        @Override
        public void run() {
            synchronized (myLock) {
                while (threadID != threadAllowedToRun) {
                    try {
                        myLock.wait();
                    } catch (InterruptedException e) {}
                }
                view.updateCell(id, row, col);
                threadAllowedToRun++;
                myLock.notifyAll();
            }
        }
    }
}

虽然这是一种可能的解决方案,但我认为基于 JavaFx 自己的工具的解决方案更可取。

您的代码不符合关注点分离原则。 (或者至少你做得不好。)

动画应该由视图和视图单独完成,而不是在控制器和视图之间协作。将调度动画的所有动画放在Viewclass.

SequentialTransition 可以将多个动画包装在一个按顺序播放的动画中,但控制器不应该关心这样做。

public class View {

    private static final double SIZE = 70;
    private static final Duration ANIMATION_DURATION = Duration.millis(600);

    private final Map<Integer, Label> labelsById = new HashMap<>(); // stores labels by id
    private final Button play = new Button("Play");
    private Pane board;
    private final HBox control;

    private final Timeline animation;
    private final LinkedList<ElementPosition> pendingAnimations = new LinkedList<>(); // stores parameter combination for update calls

    private Node animatedNode;

    public View(int[][] cellModels) { // we don't really need the whole model here
        makeBoardPane(cellModels);

        this.control = new HBox(play);
        control.setAlignment(Pos.CENTER_RIGHT);

        final DoubleProperty interpolatorValue = new SimpleDoubleProperty();

        animation = new Timeline(
                new KeyFrame(Duration.ZERO, evt -> {
                    ElementPosition ePos = pendingAnimations.removeFirst();
                    animatedNode = labelsById.get(ePos.id);

                    Point2D newLocation = getLocationByRowCol(ePos.row, ePos.column);

                    // create binding for layout pos
                    animatedNode.layoutXProperty().bind(
                            interpolatorValue.multiply(newLocation.getX() - animatedNode.getLayoutX())
                                    .add(animatedNode.getLayoutX()));
                    animatedNode.layoutYProperty().bind(
                            interpolatorValue.multiply(newLocation.getY() - animatedNode.getLayoutY())
                                    .add(animatedNode.getLayoutY()));
                }, new KeyValue(interpolatorValue, 0d)),
                new KeyFrame(ANIMATION_DURATION, evt -> {
                    interpolatorValue.set(1);

                    // remove bindings
                    animatedNode.layoutXProperty().unbind();
                    animatedNode.layoutYProperty().unbind();

                    animatedNode = null;

                    if (pendingAnimations.isEmpty()) {
                        // abort, if no more animations are pending
                        View.this.animation.stop();
                    }
                }, new KeyValue(interpolatorValue, 1d)));
        animation.setCycleCount(Animation.INDEFINITE);
    }

    private void makeBoardPane(int[][] cellModels) {
        board = new Pane();
        for (int row = 0; row < cellModels.length; row++) {
            for (int col = 0; col < cellModels[row].length; col++) {
                Point2D location = getLocationByRowCol(row, col);
                int id = cellModels[row][col];

                Label label = new Label(Integer.toString(id));
                label.setPrefSize(SIZE, SIZE);
                label.setLayoutX(location.getX());
                label.setLayoutY(location.getY());
                label.setStyle("-fx-border-color:blue");
                label.setAlignment(Pos.CENTER);

                labelsById.put(id, label);

                board.getChildren().add(label);
            }
        }
    }

    private static class ElementPosition {

        private final int id;
        private final int row;
        private final int column;

        public ElementPosition(int id, int row, int column) {
            this.id = id;
            this.row = row;
            this.column = column;
        }

    }

    public void updateCell(int id, int row, int column) {
        pendingAnimations.add(new ElementPosition(id, row, column));
        animation.play();
    }

    private static Point2D getLocationByRowCol(int row, int col) {
        return new Point2D(SIZE * col, SIZE * row);
    }

    public Pane getBoard() {
        return board;
    }

    public Pane getControlPane() {
        return control;
    }

    public Button getPlayBtn() {
        return play;
    }
}

降低复杂性的用法示例:

private int[][] oldValue;

@Override
public void start(Stage primaryStage) {
    List<Integer> values = new ArrayList<>(Arrays.asList(0, 1, 2, 3));
    oldValue = new int[][]{{0, 1}, {2, 3}};
    View view = new View(oldValue);
    Button btn = new Button("Shuffle");

    btn.setOnAction((ActionEvent event) -> {
        Collections.shuffle(values);
        int[][] newValue = new int[2][2];
        for (int i = 0; i < 2 * 2; i++) {
            newValue[i / 2][i % 2] = values.get(i);
        }
        System.out.println(values);

        for (int row = 0; row < newValue.length; row++) {
            for (int col = 0; col < newValue[row].length; col++) {
                if (newValue[row][col] != oldValue[row][col]) {
                    view.updateCell(newValue[row][col], row, col);
                }
            }
        }
        oldValue = newValue;
    });

    Scene scene = new Scene(new BorderPane(view.getBoard(), null, view.getControlPane(), btn, null));

    primaryStage.setScene(scene);
    primaryStage.show();
}

以下答案基于答案。我重构了它,将它分解成更小、更详细的 "pieces",这使它(对我而言)更容易理解。
我张贴它希望它也能帮助其他人。

import java.util.LinkedList;
import java.util.stream.IntStream;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.util.Duration;

public final class Puzzle extends Application{

    private Controller controller;
    @Override
    public void start(Stage stage) throws Exception {
        controller = new Controller();
        BorderPane root = new BorderPane(controller.getBoardPane());
        root.setTop(controller.getControlPane());
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) { launch();}
}

class Controller {

    private View view ;
    private Model model;

    Controller() {
        model = new Model();
        model.addChangeListener(getModelCangeListener());//observe model changes
        view = new View(model);
        view.getPlayBtn().setOnAction( a -> IntStream.
                                range(0,4).forEach( (i)-> shuffle()));
    }

    private ChangeListener<int[][]> getModelCangeListener() {

        return  (ObservableValue<? extends int[][]> observable,
            int[][] oldValue, int[][] newValue)-> {
                for (int row = 0; row < newValue.length ; row++) {
                    for (int col = 0; col < newValue[row].length ; col++) {
                        if(newValue[row][col] != oldValue[row][col]) {
                            view.updateCell(newValue[row][col], row, col);
                        }
                    }
                }
            };
    }

    private void shuffle() {
        int[][] modelData = model.getCellModels();
        int rows = modelData.length, columns = modelData[0].length;
        int[][] newModelData = new int[rows][columns];
        for (int row = 0; row < rows ; row ++) {
            for (int col = 0; col < columns ; col ++) {
                int colIndex = ((col+1) < columns ) ? col + 1  : 0;
                int rowIndex =  ((col+1) < columns ) ?  row : ((row + 1) < rows) ? row +1 : 0;
                newModelData[row][col] = modelData[rowIndex][colIndex];
            }
        }
        model.setCellModels(newModelData);
    }

    Pane getBoardPane() { return view.getBoardPane(); }

    Pane getControlPane() { return view.getControlPane(); }
}

class View{

    private final Timeline timeLineAnimation;// private final Timeline timeLineAnimation;
    private final LinkedList<ElementPosition> pendingAnimations = new LinkedList<>(); // stores parameter combination for update calls
    private static final Duration ANIMATION_DURATION = Duration.millis(600);
    private static final double SIZE = 70;
    private int[][] cellModels;

    private final Button play = new Button("Play");
    private Pane board, control = new HBox(play);
    Node animatedNode;

    View(Model model) {
        cellModels = model.getCellModels();
        makeBoardPane();
        ((HBox) control).setAlignment(Pos.CENTER_RIGHT);
        timeLineAnimation = new TimeLineAnimation().get();
    }

    private void makeBoardPane() {

        board = new Pane();
        for (int row = 0; row < cellModels.length ; row ++ ) {
            for (int col = 0; col < cellModels[row].length ; col ++ ) {
               int id = cellModels[row][col];
               Label label = new Label(String.valueOf(id));
               label.setPrefSize(SIZE, SIZE);
               Point2D location = getLocationByRowCol(row, col);
               label.setLayoutX(location.getX());
               label.setLayoutY(location.getY());
               label.setStyle("-fx-border-color:blue");
               label.setAlignment(Pos.CENTER);
               label.setId(String.valueOf(id));
               board.getChildren().add(label);
            }
        }
    }

    public void updateCell(int id, int row, int column) {
        pendingAnimations.add(new ElementPosition(id, row, column));
        timeLineAnimation.play();
    }

    private Node getNodeById(int id) {

        return board.getChildren().filtered(n ->
            Integer.valueOf(n.getId()) == id).get(0);
    }

    private Point2D getLocationByRowCol(int row, int col) {
        return new Point2D(SIZE * col, SIZE * row);
    }

    Pane getBoardPane() { return board; }
    Pane getControlPane() { return control;}
    Button getPlayBtn() {return play ;}

    private static class ElementPosition {

        private final int id, row, column;

        public ElementPosition(int id, int row, int column) {
            this.id = id;
            this.row = row;
            this.column = column;
        }
    }

    class TimeLineAnimation {

        private final Timeline timeLineAnimation;
        //value to be interpolated by time line
        final DoubleProperty interpolatorValue = new SimpleDoubleProperty();
        private Node animatedNode;

        TimeLineAnimation() {
            timeLineAnimation = new Timeline(getSartKeyFrame(), getEndKeyFrame());
            timeLineAnimation.setCycleCount(Animation.INDEFINITE);
        }

        // a 0 duration event, used to do the needed setup for next key frame
        private KeyFrame getSartKeyFrame() {

            //executed when key frame ends
            EventHandler<ActionEvent> onFinished = evt -> {

                //protects against "playing" when no pending animations
                if(pendingAnimations.isEmpty()) {
                    timeLineAnimation.stop();
                    return;
                };
                ElementPosition ePos = pendingAnimations.removeFirst();
                animatedNode = getNodeById(ePos.id);

                Point2D newLocation = getLocationByRowCol(ePos.row, ePos.column);

                // bind x,y layout properties interpolated property interpolation effects
                //both x and y
                animatedNode.layoutXProperty().bind(
                        interpolatorValue.multiply(newLocation.getX() - animatedNode.getLayoutX())
                                .add(animatedNode.getLayoutX()));
                animatedNode.layoutYProperty().bind(
                        interpolatorValue.multiply(newLocation.getY() - animatedNode.getLayoutY())
                                .add(animatedNode.getLayoutY()));
            };

            KeyValue startKeyValue = new KeyValue(interpolatorValue, 0d);
            return new KeyFrame(Duration.ZERO, onFinished, startKeyValue);
        }

        private KeyFrame getEndKeyFrame() {

            //executed when key frame ends
            EventHandler<ActionEvent> onFinished =evt -> {
                // remove bindings
                animatedNode.layoutXProperty().unbind();
                animatedNode.layoutYProperty().unbind();
            };

            KeyValue endKeyValue = new KeyValue(interpolatorValue, 1d);
            return new KeyFrame(ANIMATION_DURATION, onFinished, endKeyValue);
        }

        Timeline get() { return timeLineAnimation;  }
    }
}

class Model{

    private int[][] cellModels = new int[][] { {0,1}, {2,3} };
    private SimpleObjectProperty<int[][]> cellModelsProperty =
            cellModelsProperty = new SimpleObjectProperty<>(cellModels);

    void addChangeListener(ChangeListener<int[][]> listener) {
        cellModelsProperty.addListener(listener);
    }

    int[][] getCellModels() {
        return  (cellModelsProperty == null) ? null : cellModelsProperty.get();
    }

    void setCellModels(int[][] cellModels) { cellModelsProperty.set(cellModels);}
}

(将整个代码复制粘贴到运行中Puzzle.java)