有没有办法删除单元格不会刷新整个列表视图?

Mas*_*ror 1 java javafx

我使用两个 JavaFX Timeline 对象在 ListCell 内创建一个倒数计时器和一个进度条(即缩小的 JavaFX 矩形)。当计时器达到零时,矩形的宽度变为零,并且从 ListView 中移除单元格。但是,整个 ListView 会自动刷新,从而导致其他单元格中的时间轴重置。这是从两个单元格开始的样子:

运行时示例

在代码中,updateItem方法从模型中获取秒数并设置计时器标签的文本。当一个信元从ListView中移除,的updateItem调用与含有复位时间线上的字符串“20”一个CellData对象。但我希望带有未完成计时器的单元格继续;不是从头开始。这是最小的、可重现的示例:

public class AnimatedCells extends Application {
public class MyCell extends ListCell<CellData> {
    private CellComponent cc;
    private Timeline rectTimeline, timerTimeline;
    private KeyValue rectWidth;

    public MyCell() {
        cc = new CellComponent();
        rectTimeline = new Timeline();
        timerTimeline = new Timeline();
        rectWidth = new KeyValue( cc.getRect().widthProperty(), 0 );
    }

    @Override
    protected void updateItem( CellData item, boolean empty ) {
        super.updateItem(item, empty);

        if (empty || item == null) {
            setText(null);
            setGraphic(null);
        } else {
            if ( item.getData() != null ) {
                System.out.println(item.getData());

                cc.getTimer().setText( item.getData() );

                rectTimeline.getKeyFrames().add( new KeyFrame( Duration.seconds( Double.parseDouble( cc.getTimer().getText() ) ), rectWidth ) );
                timerTimeline.getKeyFrames().addAll(
                        new KeyFrame( Duration.seconds( 0 ),
                            event -> {
                                int timerSeconds = Integer.parseInt( cc.getTimer().getText() );

                                if ( timerSeconds > 0 ) {
                                    cc.getTimer().setText( Integer.toString(--timerSeconds) );
                                }
                                else {
                                    rectTimeline.stop();
                                    timerTimeline.stop();
                                    super.getListView().getItems().remove(this.getItem());
                                }
                        }),
                        new KeyFrame( Duration.seconds( 1 ) ) );
                timerTimeline.setCycleCount( Animation.INDEFINITE );
                setGraphic( cc.getCellPane() );
                rectTimeline.play();
                timerTimeline.play();
            }
        }
    }
}

public class CellComponent {
    @FXML
    private Pane cellPane;

    @FXML
    private Label timer;

    @FXML
    private Rectangle rect;

    public CellComponent() {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/listcell.fxml"));
        fxmlLoader.setController(this);
        try
        {
            fxmlLoader.load();
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
    }

    public Pane getCellPane() {
        return cellPane;
    }

    public void setCellPane(Pane cellPane) {
        this.cellPane = cellPane;
    }

    public Label getTimer() {
        return timer;
    }

    public void setTimer(Label timer) {
        this.timer = timer;
    }

    public Rectangle getRect() {
        return rect;
    }

    public void setRect(Rectangle rect) {
        this.rect = rect;
    }

}

public class CellData {
    private String data;

    public CellData() {
        super();
    }

    public CellData(String data) {
        super();
        this.setData(data);
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

}

@FXML
private ListView<CellData> listView;

private ObservableList<CellData> observableList = FXCollections.observableArrayList();

public void initialize() {
    observableList.add( new CellData("20") );
    observableList.add( new CellData("10") );

    listView.setItems( observableList );
    listView.setCellFactory( listView -> {
        MyCell cell = new MyCell();
        return cell;
    });
}

@Override
public void start(Stage stage) throws Exception {
    Parent root = new FXMLLoader(getClass().getResource("/fxml/listmanager.fxml")).load();
    Scene scene = new Scene(root);
    stage.setScene(scene);
    stage.show();
}

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

}
Run Code Online (Sandbox Code Playgroud)

另外,当只有两行数据时,为什么在启动时调用了 3 次updateItem?上面代码中的单独打印语句输出:

20
20
10

我正在使用 Java 11.0.6。

VGR*_*VGR 5

Cell 的工作是确定如何显示数据。不能在 Cell 的updateItem方法中更改底层数据,因为单个 Cell 实例可以用于多个值。

来自Cell 的文档

由于 TreeView、ListView、TableView 和其他此类控件可能用于显示大量数据,因此为控件中的每一项都创建一个实际的 Cell 是不可行的。我们仅使用很少的 Cell 来表示极大的数据集。每个 Cell 都被“回收”或重复使用。

您需要将所有时间线移动到 CellData 对象中。并且 Cell 不得修改这些时间轴或 ListView 的项目。

使数据对象具有可观察的属性是习惯和有用的,因此可视化组件可以将可视化属性绑定到它们。

CellData 对象不应该知道包含它们的 ListView。您可以为每个 CellData 实例提供一个 Consumer 或其他执行删除数据工作的函数。

所以,你会想要这样的数据类:

public static class CellData {
    private final StringProperty data;

    private final ReadOnlyDoubleWrapper secondsRemaining;

    private final Timeline timeline = new Timeline();

    public CellData(Consumer<? super CellData> finished) {
        data = new SimpleStringProperty(this, "data");
        secondsRemaining =
            new ReadOnlyDoubleWrapper(this, "secondsRemaining");

        data.addListener((o, oldData, newData) -> {
            try {
                double startTime = Double.parseDouble(newData);

                timeline.stop();

                timeline.getKeyFrames().setAll(
                    new KeyFrame(Duration.ZERO,
                        new KeyValue(secondsRemaining, startTime)),
                    new KeyFrame(Duration.seconds(startTime),
                        new KeyValue(secondsRemaining, 0.0))
                );
                timeline.setOnFinished(e -> finished.accept(this));

                timeline.play();
            } catch (NumberFormatException e) {
                System.err.println(
                    "Cannot start timer for invalid seconds value: " + e);
                Platform.runLater(() -> finished.accept(this));
            }
        });
    }

    public CellData(String data,
                    Consumer<? super CellData> finished) {
        this(finished);
        this.setData(data);
    }

    public StringProperty dataProperty() {
        return data;
    }

    public String getData() {
        return data.get();
    }

    public void setData(String data) {
        this.data.set(data);
    }

    public ReadOnlyDoubleProperty secondsRemainingProperty() {
        return secondsRemaining.getReadOnlyProperty();
    }

    public double getSecondsRemaining() {
        return secondsRemaining.get();
    }
}
Run Code Online (Sandbox Code Playgroud)

一个实例将用这样的东西构造:

new CellData(secondsText,
    c -> list.getItems().remove(c)));
Run Code Online (Sandbox Code Playgroud)

这允许单元类更简单:

public static class MyCell extends ListCell<CellData> {
    private CellComponent cc;

    public MyCell() {
        cc = new CellComponent();
    }

    @Override
    protected void updateItem(CellData item, boolean empty) {
        super.updateItem(item, empty);

        if (empty || item == null) {
            setText(null);
            setGraphic(null);
        } else {
            cc.getTimer().textProperty().unbind();
            cc.getTimer().textProperty().bind(
                item.secondsRemainingProperty().asString("%.0f"));

            cc.getRect().widthProperty().unbind();
            cc.getRect().widthProperty().bind(
                item.secondsRemainingProperty().multiply(10));

            setText(null);
            setGraphic(cc.getCellPane());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

把它们放在一起看起来像这样。(为了简单性和可重复性,我用内联代码替换了 FXML。)

import java.util.function.Consumer;

import javafx.application.Application;
import javafx.application.Platform;

import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;

import javafx.beans.property.StringProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;

import javafx.geometry.Insets;
import javafx.geometry.Pos;

import javafx.stage.Stage;
import javafx.scene.Scene;

import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
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.scene.shape.Rectangle;

import javafx.util.Duration;
import javafx.util.converter.DoubleStringConverter;

public class AnimatedCells
extends Application {
    @Override
    public void start(Stage stage) {
        ListView<CellData> list = new ListView<>();
        list.setCellFactory(l -> new MyCell());

        TextField secondsField = new TextField();

        Button newTimerButton = new Button("Create");
        newTimerButton.setOnAction(e -> {
            list.getItems().add(
                new CellData(secondsField.getText(),
                    c -> list.getItems().remove(c)));
        });
        secondsField.setOnAction(e -> {
            list.getItems().add(
                new CellData(secondsField.getText(),
                    c -> list.getItems().remove(c)));
        });

        HBox newButtonPane = new HBox(6, secondsField, newTimerButton);
        newButtonPane.setPadding(new Insets(12));

        stage.setScene(new Scene(
            new BorderPane(list, null, null, newButtonPane, null)));
        stage.setTitle("Animated Cells");
        stage.show();
    }

    public static class MyCell extends ListCell<CellData> {
        private CellComponent cc;

        public MyCell() {
            cc = new CellComponent();
        }

        @Override
        protected void updateItem(CellData item, boolean empty) {
            super.updateItem(item, empty);

            if (empty || item == null) {
                setText(null);
                setGraphic(null);
            } else {
                cc.getTimer().textProperty().unbind();
                cc.getTimer().textProperty().bind(
                    item.secondsRemainingProperty().asString("%.0f"));

                cc.getRect().widthProperty().unbind();
                cc.getRect().widthProperty().bind(
                    item.secondsRemainingProperty().multiply(10));

                setText(null);
                setGraphic(cc.getCellPane());
            }
        }
    }

    public static class CellData {
        private final StringProperty data;

        private final ReadOnlyDoubleWrapper secondsRemaining;

        private final Timeline timeline = new Timeline();

        public CellData(Consumer<? super CellData> finished) {
            data = new SimpleStringProperty(this, "data");
            secondsRemaining =
                new ReadOnlyDoubleWrapper(this, "secondsRemaining");

            data.addListener((o, oldData, newData) -> {
                try {
                    double startTime = Double.parseDouble(newData);

                    timeline.stop();

                    timeline.getKeyFrames().setAll(
                        new KeyFrame(Duration.ZERO,
                            new KeyValue(secondsRemaining, startTime)),
                        new KeyFrame(Duration.seconds(startTime),
                            new KeyValue(secondsRemaining, 0.0))
                    );
                    timeline.setOnFinished(e -> finished.accept(this));

                    timeline.play();
                } catch (NumberFormatException e) {
                    System.err.println(
                        "Cannot start timer for invalid seconds value: " + e);
                    Platform.runLater(() -> finished.accept(this));
                }
            });
        }

        public CellData(String data,
                        Consumer<? super CellData> finished) {
            this(finished);
            this.setData(data);
        }

        public StringProperty dataProperty() {
            return data;
        }

        public String getData() {
            return data.get();
        }

        public void setData(String data) {
            this.data.set(data);
        }

        public ReadOnlyDoubleProperty secondsRemainingProperty() {
            return secondsRemaining.getReadOnlyProperty();
        }

        public double getSecondsRemaining() {
            return secondsRemaining.get();
        }
    }

    public static class CellComponent {
        private final Pane cellPane;

        private final Label timer;

        private final Rectangle rect;

        public CellComponent() {
            // For the sake of example, I'm building this in code rather than
            // with FXML.

            //FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/listcell.fxml"));
            //fxmlLoader.setController(this);
            //try
            //{
            //    fxmlLoader.load();
            //}
            //catch (IOException e)
            //{
            //    throw new RuntimeException(e);
            //}

            rect = new Rectangle(1, 40);
            rect.setArcWidth(20);
            rect.setArcHeight(20);
            rect.setStyle(
                "-fx-fill: red; -fx-stroke: black; -fx-stroke-width: 2;");
            timer = new Label(" ");
            timer.setStyle("-fx-font-size: 18pt; -fx-alignment: center;");
            cellPane = new HBox(24, timer, rect);
            cellPane.setStyle("-fx-alignment: center-left;");
        }

        public Pane getCellPane() {
            return cellPane;
        }

        public Label getTimer() {
            return timer;
        }

        public Rectangle getRect() {
            return rect;
        }
    }

    public static class Main {
        public static void main(String[] args) {
            Application.launch(AnimatedCells.class, args);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)