对Javafx中的新ListView条目进行动画处理 - java

问题
你好
我正在尝试编写一个应用程序,以使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%;
    }
    

    参考方案

    为了找到导致监听器再次触发的原因,我在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()中的addTrailingCells()VirtualFlow.layoutChildren()再次重新构建所有单元格。

    因此,根据设计,无法避免这些调用,这意味着如果要确保动画仅被调用一次,则动画不应出现在侦听器中。

    动画

    我提供了一个解决方案,它意味着获取单元并运行动画:

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

    这不是非常可取,因为使用了私有API VirtualFlow

    通过寻找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);
        }
    
    }
    

    Java-搜索字符串数组中的字符串 - java

    在Java中,我们是否有任何方法可以发现特定字符串是字符串数组的一部分。我可以避免出现一个循环。例如String [] array = {"AA","BB","CC" }; string x = "BB" 我想要一个if (some condition to tell wheth…

    Java Applet的URLConnection与PHP无效 - java

    我已经研究了Oracle文档和示例,但仍然无法正常工作。我有一个Java Applet,它只是尝试使用URLConnection和OutputStreamWriter通过POST将文本字段发送到PHP脚本。 Java方面似乎工作正常,没有引发异常,但是PHP在我的页面上未显示任何输出。我是PHP新手,因此请耐心等待。这是相关的Java部分: try { UR…

    Java Scanner读取文件的奇怪行为 - java

    因此,在使用Scanner类从文件读取内容时,我遇到了一个有趣的问题。基本上,我试图从目录中读取解析应用程序生成的多个输出文件,以计算一些准确性指标。基本上,我的代码只是遍历目录中的每个文件,并使用扫描仪将其打开以处理内容。无论出于何种原因,扫描程序都不会读取其中的一些文件(所有UTF-8编码)。即使文件不是空的,scanner.hasNextLine()在…

    Java Globbing模式以匹配目录和文件 - java

    我正在使用递归函数遍历根目录下的文件。我只想提取*.txt文件,但不想排除目录。现在,我的代码如下所示:val stream = Files.newDirectoryStream(head, "*.txt") 但是这样做将不会匹配任何目录,并且返回的iterator()是False。我使用的是Mac,所以我不想包含的噪音文件是.DS_ST…

    直接读取Zip文件中的文件-Java - java

    我的情况是我有一个包含一些文件(txt,png,...)的zip文件,我想直接按它们的名称读取它,我已经测试了以下代码,但没有结果(NullPointerExcepion):InputStream in = Main.class.getResourceAsStream("/resouces/zipfile/test.txt"); Buff…