在Javafx中为新的ListView条目设置动画

问题

嗨,

我正在尝试编写一个应用程序,ListView中的每个新条目都会被动画化。 这是我的代码:

public class BookCell extends ListCell { private Text text; private HBox h; public BookCell() { this.text = new Text(); this.h = new HBox(); this.h.getChildren().add(text); super.getStyleClass().add("book-list-cell"); super.itemProperty().addListener((obs,oldv,newv)->{ if(newv != null ) { if(getIndex() == this.getListView().getItems().size()-1 ) { //why does this get called twice for each update? System.out.println("isbn = "+newv.getIsbn().get() + " lastIndexOf=" + this.getListView().getItems().lastIndexOf(newv)+" Index="+getIndex()+" size="+this.getListView().getItems().size()); runAnimation(); } } }); this.getChildren().add(h); } @Override protected void updateItem(Book item, boolean empty) { super.updateItem(item, empty); if(!empty) super.setGraphic(h); text.setText(item == null ? "" : item.getIsbn().get()); } public void runAnimation() { FadeTransition f = new FadeTransition(); f.setFromValue(0); f.setToValue(1); f.setNode(this); f.play(); } } 

我试图在itemProperty中添加一个监听器,但是我得到了一些奇怪的行为。 首先,每次添加条目时,监听器都会触发两次: 在此处输入图像描述 这导致动画播放两次。 即使在这种情况下几乎不可察觉,这可能有点尴尬。

此外,在列表开始滚动之后, 有时会为可见条目重复动画: 在此处输入图像描述

说实话,如果一个项目再次可见 ,我不一定会想到动画重复,但就目前而言,单元格动画是非常不可靠的。

我知道细胞是虚拟控制的并且随着时间的推移会重复使用。 但是我很难找到一种可靠的方法来确定屏幕上是否出现非空单元格。

问题

  1. 为什么听众会两次开火?
  2. 观察屏幕上单元格出现/消失的最佳方法是什么
  3. 有没有更好的方法将动画添加到ListView?

完整的代码

它可能会有所帮助,所以这里是我的CSS和主文件。

Main.java

 public class Main extends Application { static int newBookIndex = 1; public static final ObservableList data = FXCollections.observableArrayList(); @Override public void start(Stage primaryStage) { try { GridPane myPane = (GridPane)FXMLLoader.load(getClass().getResource("quotes.fxml")); ListView lv = (ListView) myPane.getChildren().get(0); Button addButton = (Button) myPane.getChildren().get(1); addButton.setText("Add Book"); addButton.setOnAction((event)->{ data.add(new Book(String.valueOf(newBookIndex++),"test")); }); lv.setEditable(true); data.addAll( new Book("123","Hugo"), new Book("456","Harry Potter") ); lv.setItems(data); lv.setCellFactory((param)->return new BookCell()); Scene myScene = new Scene(myPane); myScene.getStylesheets().add(getClass().getResource("application.css").toExternalForm()); primaryStage.setScene(myScene); primaryStage.show(); } catch(Exception e) { e.printStackTrace(); } } public static void main(String[] args) { launch(args); } } 

application.css(基于https://github.com/privatejava/javafx-listview-animation )

 .book-list-cell:odd{ -fx-background-image: url('./wooden2.png') ,url('./gloss.png'); -fx-background-repeat: repeat,no-repeat; } .book-list-cell:empty { -fx-background-image: url('./gloss.png'); -fx-background-repeat: repeat; } .book-list-cell { -fx-font-size:45px; -fx-font-family: 'Segoe Script'; } .book-list-cell:selected,.book-list-cell:selected:hover{ -fx-border-color:linear-gradient(to bottom,transparent,rgb(22,22,22,1)); -fx-border-width:2 2 2 10px; -fx-effect:innershadow( three-pass-box,#764b0c,30,0.3,0,0); } .book-list-cell:hover{ -fx-border-color:rgb(255,255,255,0.7); -fx-border-width:1 1 1 10px; } .book-list-cell{ -fx-background-image: url('./wooden.png') ,url('./gloss.png'); -fx-background-repeat: repeat,no-repeat; -fx-background-position:left top; -fx-background-size: auto,100% 40%; } 

为了找到监听器双重触发的原因,我在layoutChildren调用之后做了一些调试。

简而言之:对于Add Book按钮的每次单击, itemProperty()中没有两个,而是三个更改。 要查找,请在侦听器和updateItem方法上向控制台添加一些打印:

 itemProperty().addListener((obs,oldv,newv)->{ System.out.println("change: "+getIndex()+", newv "+(newv != null?newv.getIsbn():"null")+", oldv: "+(oldv != null?oldv.getIsbn():"null")); } protected void updateItem(Book item, boolean empty){ super.updateItem(item, empty); System.out.println("update item: "+(item!=null?item.getIsbn():"null")); } 

对于第一本书“123”,这是跟踪:

 update item: null change: 0, newv 123, oldv: null update item: 123 change: -1, newv null, oldv: 123 update item: null update item: null change: 0, newv 123, oldv: null update item: 123 

由于创建了新的单元格,第一次更改按预期发生。 但是一旦它被创建,之后是对索引为-1的VirtualFlow.releaseCell()的调用,最后使用VirtualFlow.releaseCell() addLeadingCells()addTrailingCells()重新addLeadingCells() 所有单元格。

因此,通过设计,这些调用无法避免,如果您想确保它只被调用一次,那么动画不应该在监听器中。

动画

我有一个解决方案,意味着获取单元格并运行动画:

 addButton.setOnAction((event)->{ data.add(new Book(String.valueOf(newBookIndex++),"test")); VirtualFlow vf=(VirtualFlow)lv.lookup(".virtual-flow"); BookCell cell = (BookCell)vf.getCell(lv.getItems().size()-1); cell.runAnimation(); }); 

这是不可取的,因为使用VirtualFlow ,私有API。

通过查找CSS选择器indexed-cells可以避免这种情况:

 addButton.setOnAction((event)->{ data.add(new Book(String.valueOf(newBookIndex),"test")); lv.lookupAll(".indexed-cell").stream() .map(n->(BookCell)n) .filter(c->c.getBook().getIsbn().equals(String.valueOf(newBookIndex))) .findFirst().ifPresent(BookCell::runAnimation); newBookIndex++; }); 

请注意,由于我们正在使用查找,因此应在显示阶段后添加按钮的事件处理程序。

编辑

OP滚动报告了一些奇怪的问题。 我不确定这是不是一个错误,但顶部单元格是动画而不是底部单元格。

为了解决这个问题,我采用了这种解决方法,删除BookCell动画,并为单元格创建一个,使用VirtualFlow获取单元格并在必要时滚动。

首先,我们尝试查找滚动条是否不可见:我们使用淡入淡出过渡。 但是如果我们有滚动条,现在我们需要调用show()来滚动到最后一个单元格。 在启动淡入淡出过渡之前,短暂的PauseTransition执行此操作。

 addButton.setOnAction((event)->{ addButton.setDisable(true); data.add(new Book(String.valueOf(newBookIndex),"test")); VirtualFlow vf=(VirtualFlow)lv.lookup(".virtual-flow"); if(!lv.lookup(".scroll-bar").isVisible()){ FadeTransition f = new FadeTransition(); f.setDuration(Duration.seconds(1)); f.setFromValue(0); f.setToValue(1); f.setNode(vf.getCell(lv.getItems().size()-1)); f.setOnFinished(t->{ newBookIndex++; addButton.setDisable(false); }); f.play(); } else { PauseTransition p = new PauseTransition(Duration.millis(20)); p.setOnFinished(e->{ vf.getCell(lv.getItems().size()-1).setOpacity(0); vf.show(lv.getItems().size()-1); FadeTransition f = new FadeTransition(); f.setDuration(Duration.seconds(1)); f.setFromValue(0); f.setToValue(1); f.setNode(vf.getCell(lv.getItems().size()-1)); f.setOnFinished(t->{ newBookIndex++; addButton.setDisable(false); }); f.play(); }); p.play(); } }); 

更新的代码

最后,这是我使用过的所有代码:

 public class Main extends Application { private int newBookIndex = 2; public final ObservableList data = FXCollections.observableArrayList( new Book("123","Hugo"), new Book("456","Harry Potter")); private final ListView lv = new ListView<>(); @Override public void start(Stage primaryStage) { Button addButton = new Button("Add Book"); lv.setCellFactory(param->new BookCell()); lv.setItems(data); Scene myScene = new Scene(new VBox(10,lv,addButton), 200, 200); myScene.getStylesheets().add(getClass().getResource("application.css").toExternalForm()); primaryStage.setScene(myScene); primaryStage.show(); addButton.setOnAction((event)->{ addButton.setDisable(true); data.add(new Book(String.valueOf(newBookIndex),"test")); VirtualFlow vf=(VirtualFlow)lv.lookup(".virtual-flow"); if(!lv.lookup(".scroll-bar").isVisible()){ FadeTransition f = new FadeTransition(); f.setDuration(Duration.seconds(1)); f.setFromValue(0); f.setToValue(1); f.setNode(vf.getCell(lv.getItems().size()-1)); f.setOnFinished(t->{ newBookIndex++; addButton.setDisable(false); }); f.play(); } else { PauseTransition p = new PauseTransition(Duration.millis(20)); p.setOnFinished(e->{ vf.getCell(lv.getItems().size()-1).setOpacity(0); vf.show(lv.getItems().size()-1); FadeTransition f = new FadeTransition(); f.setDuration(Duration.seconds(1)); f.setFromValue(0); f.setToValue(1); f.setNode(vf.getCell(lv.getItems().size()-1)); f.setOnFinished(t->{ newBookIndex++; addButton.setDisable(false); }); f.play(); }); p.play(); } }); } class BookCell extends ListCell{ private final Text text = new Text(); private final HBox h = new HBox(text); { getStyleClass().add("book-list-cell"); } @Override protected void updateItem(Book item, boolean empty){ super.updateItem(item, empty); if(item!=null && !empty){ text.setText(item.getIsbn()); setGraphic(h); } else { setGraphic(null); setText(null); } } } class Book { private Book(String isbn, String name) { this.isbn.set(isbn); this.name.set(name); } private final StringProperty name = new SimpleStringProperty(); public String getName() { return name.get(); } public void setName(String value) { name.set(value); } public StringProperty nameProperty() { return name; } private final StringProperty isbn = new SimpleStringProperty(); public String getIsbn() { return isbn.get(); } public void setIsbn(String value) { isbn.set(value); } public StringProperty isbnProperty() { return isbn; } } public static void main(String[] args) { launch(args); } }