【问题标题】:Is there any way the removal of a cell doesn't refresh the entire listview?有没有办法删除一个单元格不会刷新整个列表视图?
【发布时间】:2023-03-06 15:17:01
【问题描述】:

我使用两个 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);
}

}

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

20
20
10

我使用的是 Java 11.0.6。

【问题讨论】:

  • updateItem() 何时被调用的细节是一个故意不公开的实现细节;但一般来说,它可以在列表视图的生命周期内频繁调用。例如,当用户滚动列表单元格时,这些单元格将被重新用于显示不同的项目,并调用 updateItem() 以使用不同的项目更新单元格。在这种情况下,您显然不想重新启动计时器。通常,计时器的当前值是正在显示的 数据 的一部分,因此它应该是 CellData 类的一部分。
  • 我认为这里真正的问题是“您真正希望计时器何时启动?”。显然不是在调用 updateItem() 时(因为你不知道,也不可能真正知道什么时候发生),但你从不告诉我们什么时候发生。
  • 我希望在启动时视图准备就绪时启动计时器。如果我要在运行时向列表中添加第三个单元格,我还希望启动一个计时器。
  • 大概包括添加到列表中但由于滚动到视图之外而不可见的项目?
  • 是的,如果单元格被添加到列表中但不可见,计时器应该启动。

标签: java javafx


【解决方案1】:

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

来自the documentation for Cell

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

您需要将所有时间线移动到 CellData 对象中。并且 Cell 不得修改那些 Timelines 或 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();
    }
}

一个实例可以这样构造:

new CellData(secondsText,
    c -> list.getItems().remove(c)));

这使得单元类更加简单:

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

把它们放在一起看起来像这样。 (为了简单和可重复性,我已将 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);
        }
    }
}

【讨论】:

  • 太棒了!是否可以向单元格添加第二个矩形和计时器,以便当右侧的矩形和计时器变为零时,左侧的矩形和计时器开始播放?计时器可以有不同的长度,只有当左侧的矩形和计时器变为零时,才会删除单元格。我正在为如何修改 CellData 来做到这一点而苦苦挣扎。
  • 听起来你可能想要一个 TableView 而不是 ListView,所以每行可以有两列。每个 TableColumn 都可以有自己的单元工厂。听起来每个 CellData 实例都需要有两个时间线。
  • 我正在尝试修改您的代码以使用 TableView,但我无法在表格单元格中显示任何内容。我以前从未使用过 TableView,而且我很难理解 Internet 上的文档和示例。 TableColumn 和 TableCell 对象的类型参数 应该是什么?
  • 首先,启用所有编译器警告并注意它们。这样,你就会知道你是否弄错了任何类型。如果您没有看到任何单元格,您可能忘记将 TableColumns 添加到 TableView 的columns listS 是整个表格行(TableView 的项目)的对象类型; T 是该特定列中显示的数据类型。
【解决方案2】:

如果有人感兴趣,我最终会使用单列、多行的 GridPane。没有updateItem 方法可以应对,所以我可以更好地控制一切。结果是,网格中的每一行都是独立的,这就是我想要的。我可以在同一行中有多个倒数计时器和进度条。更重要的是,我可以在不重置所有正在运行的时间线的情况下添加或删除一行。 GridPane API 也更容易使用。

【讨论】:

  • 我可以在不重置所有正在运行的时间线的情况下添加或删除一行重置不是列表/单元格固有的问题,而是由于您的实施不正确,只是说:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-04-16
  • 2023-02-11
  • 1970-01-01
  • 2011-03-24
  • 1970-01-01
  • 2018-05-07
  • 1970-01-01
相关资源
最近更新 更多