在Javafx中为新的ListView条目设置动画 [英] Animate new ListView entries in Javafx

查看:615
本文介绍了在Javafx中为新的ListView条目设置动画的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问题





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

 公共类BookCell扩展了ListCell< Book> 
{
private text text;
私人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)
{
//为什么每次更新都会调用两次?
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文件。



Main.java

 公共类主要扩展应用程序
{
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(
新书(123,雨果),
新书(456,哈利波特)
);

lv.setItems(data);
lv.setCellFactory((param) - >返回新的BookCell());
场景myScene =新场景(myPane);
myScene.getStylesheets()。add(getClass()。getResource(application.css)。toExternalForm());
primaryStage.setScene(myScene);
primaryStage.show();
}
catch(例外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:重复,不重复;
}
.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(三通盒子,#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:重复,不重复;
-fx-background-position:left top;
-fx-background-size:auto,100%40%;
}


解决方案

找到原因监听器的双重触发,我已经完成了一些调试,跟随 layoutChildren 调用。



长话短说:对于添加图书按钮的每次点击, 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,这个是跟踪:

 更新项目:null 
更改:0,newv 123,oldv:null
更新项目:123
更改:-1,newv null,oldv:123
更新项目:null
更新项目:null
更改:0,newv 123,oldv:null
更新项目:123

由于创建了新单元格,第一次更改按预期发生但是一旦它被创造出来,这就是追随者d通过调用带有索引-1的 VirtualFlow.releaseCell(),最后所有,再次使用<$ c $重建单元格c> addLeadingCells()和 addTrailingCells() in VirtualFlow.layoutChildren()



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

动画



我提供了一个解决方案,意味着获取细胞和运行动画:

  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()大小() - 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(
新书(123,雨果),新书(456,哈利波特));
私人最终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);
场景myScene =新场景(新的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();
}
});
}

类BookCell扩展了ListCell< Book> {
private final Text text = new Text();
私人最终HBox h =新HBox(文字);

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

}


The Problem

Hi,

I'm trying to write an application where every new entry in a ListView gets animated. Here is my code:

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

I've tried to add a listener to the itemProperty, but I get some strange behavior. Firstly, the listener fires twice each time I add an entry: which results in the animation playing twice. This can be a little awkward to watch even though it is hardly noticeable in this case.

Furthermore, after the list starts scrolling, the animation is sometimes repeated for a visible entry:

To be completely honest, I wouldn't necessarily mind the animation repeating if an item gets visible again, but as it stands, it is very unreliable which cells get animated.

I know that the cells are virtually controlled and get reused over time. But I'm having a hard time to find a reliable way to determine if a nonempty cell appears on the screen.

The Questions

  1. Why is the listener firing twice?
  2. What is the best way to observe the appearance/disappearance of a cell on the screen
  3. Is there a better way to add animations to a ListView?

The complete code

It might be helpful, so here are my css and main files.

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 (based on 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%;
}

解决方案

To find the reason for the double firing of the listener, I've done some debugging, following the layoutChildren calls.

Long story short: for every click on the Add Book button, there are not two, but three changes in itemProperty(). To find out, add some printing to console on the listener and the updateItem method:

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

For the first book "123", this is the trace:

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

The first change happens as expected due to the creation of a new cell. But once it's created, this is followed by a call to VirtualFlow.releaseCell() with index -1, and finally all the cells are rebuilt all over again with addLeadingCells() and addTrailingCells() in VirtualFlow.layoutChildren().

So by design, these calls can't be avoided, what means the animation shouldn't be in the listener if you want to make sure it's only called once.

ANIMATION

I've come with a solution that implies getting the cell and run the animation:

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

This is not very advisable since uses VirtualFlow, private API.

This can be avoided by looking for the CSS selector indexed-cells instead:

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

Note that since we're using lookups, the event handler for the button should be added after the stage is shown.

EDIT

There are weird problems reported by the OP with scrolling. I'm not sure if this is a bug, but the top cell is animated instead of the bottom one.

To solve this issue I've come with this workaround, removing BookCell animation, and creating one for the cell, using VirtualFlow to get the cell and scroll if necessary.

First we try to find if the scrollbars are not visible: we use the our fade transition. But if we have scrollbars, now we need to call show() to scroll to the last cell. A short PauseTransition does the trick before launching the fade transition.

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

UPDATED CODE

Finally, this is all the code I've used:

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

}

这篇关于在Javafx中为新的ListView条目设置动画的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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