在 Javafx 中为新的 ListView 条目设置动画
Posted
技术标签:
【中文标题】在 Javafx 中为新的 ListView 条目设置动画【英文标题】:Animate new ListView entries in Javafx 【发布时间】:2015-01-23 16:34:31 【问题描述】:问题
嗨,
我正在尝试编写一个应用程序,其中 ListView 中的每个新条目都会被动画化。这是我的代码:
public class BookCell extends ListCell<Book>
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 添加侦听器,但出现了一些奇怪的行为。首先,每次我添加一个条目时,监听器都会触发两次: 这导致动画播放两次。尽管在这种情况下几乎不明显,但观看起来可能会有些尴尬。
此外,在列表开始滚动后,动画有时会为可见条目重复:
老实说,如果一个项目再次可见,我不一定会介意动画重复,但就目前而言,哪些单元格被动画是非常不可靠的。
我知道这些单元实际上是受控的,并且随着时间的推移会被重复使用。但是我很难找到一种可靠的方法来确定屏幕上是否出现非空单元格。
问题
-
为什么监听器会触发两次?
观察屏幕上单元格出现/消失的最佳方法是什么
有没有更好的方法向 ListView 添加动画?
完整代码
这可能会有所帮助,所以这是我的 css 和主文件。
Main.java
public class Main extends Application
static int newBookIndex = 1;
public static final ObservableList<Book> data = FXCollections.observableArrayList();
@Override
public void start(Stage primaryStage)
try
GridPane myPane = (GridPane)FXMLLoader.load(getClass().getResource("quotes.fxml"));
ListView<Book> lv = (ListView<Book>) 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%;
【问题讨论】:
只是一个旁注。到目前为止,我不知道您可以使用您在问题中使用的标题。感谢您的展示! :D 【参考方案1】:为了找出监听器双重触发的原因,我在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()
,最后所有单元格会用addLeadingCells()
和VirtualFlow.layoutChildren()
中的addTrailingCells()
重新构建。
所以从设计上讲,这些调用是不可避免的,这意味着如果你想确保动画只被调用一次,那么它不应该在监听器中。
动画
我提供了一个解决方案,意味着获取单元格并运行动画:
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<Book> data = FXCollections.observableArrayList(
new Book("123","Hugo"), new Book("456","Harry Potter"));
private final ListView<Book> 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<Book>
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);
【讨论】:
感谢您的回答。我什至没有考虑查找方法。但是我对这个解决方案有一些问题。请您看一下这个屏幕截图:postimg.org/image/tj3f42j23 如您所见,滚动开始后,顶部单元格而不是底部单元格开始动画。 这是一个很难解决的问题。经过多次尝试,我找到了一个对我有用的解决方法。试试看,让我知道。 是的,它解决了问题。老实说,我认为将简单的动画添加到 ListView 不会那么复杂和 hacky。真的有这么不寻常的事情吗? 对于单个单元格是因为虚拟流...但是如果将动画应用于所有单元格会更简单。看看这个post。它在updateItem()
方法上播放所有单元格的动画。
是的,这篇文章是我的出发点。但是,当我运行他的示例 (available on github) 时,我的单元格到处翻转,不像视频中那样。它似乎坏了,至少在 Java 8 中是这样。以上是关于在 Javafx 中为新的 ListView 条目设置动画的主要内容,如果未能解决你的问题,请参考以下文章
结合 javafx 2 ListView 和 GridPane 功能
如何在IOS 11中为新的大型条设置渐变颜色的UINavigationbar?
在visual studio 2010中为新的QOBJECT文件生成MOC
如何在 NEW iTunes Connect 中为新应用添加图标