1

I'm learning JavaFX, and is now looking at the TableView. I want to place stock prices in a table, and flash/blink the cell background on update.

I have searched a lot, and seen a few suggestions, however not been able to find what I'm looking for. Also, I seem to have trouble understanding the underlying logic of TableViews, specifically when and how cells are created and updated.

Consider the following code (mostly borrowed from http://jaakkola.net/juhani/blog/?p=233) :

public class FlashingTableCell<S, T> extends TableCell<S, T> {

    private static final Color INCREASE_HIGHLIGHT_COLOR = Color.rgb(0, 255, 0, 0.8);
    private static final Color DECREASE_HIGHLIGHT_COLOR = Color.rgb(255, 0, 0, 0.8);
    private static final Color HIGHLIGHT_COLOR = Color.rgb(0, 255, 0, 0.8);
    private static final Duration HIGHLIGHT_TIME = Duration.millis(600);

    private final Background bgIncrease = new Background(new BackgroundFill(INCREASE_HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));
    private final Background bgDecrease = new Background(new BackgroundFill(DECREASE_HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));
    private final Background bgChange = new Background(new BackgroundFill(HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));

    private final BorderPane background = new BorderPane();
    private final Label lblText = new Label("");
    private final FadeTransition animation = new FadeTransition(HIGHLIGHT_TIME, background);

    private final StackPane container = new StackPane();

    private T prevValue;
    private S prevItem;

    final private Comparator<T> comparator;


    public FlashingTableCell(Comparator<T> comparator, Pos alignment) {
        super();
        this.comparator = comparator;

        lblText.textProperty().bindBidirectional(textProperty());
        this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

        setPadding(Insets.EMPTY);
        container.getChildren().addAll(background, lblText);
        container.setAlignment(alignment);
        setGraphic(container);
    }

    @Override
    protected void updateItem(T value, boolean empty) {
        super.updateItem(value, empty);

        System.out.println("updateItem " + this.hashCode() + " " + getIndex() + " value=" + value + " (" + prevValue + ")" + empty);

        S currentItem = getTableRow() != null && getTableRow().getItem() != null ? (S) getTableRow().getItem() : null;

        /*
         * We check that the value has been updated and that the row model/item
         * under the cell is the same. JavaFX table reuses cells so item is not
         * always the same!
         */
        boolean valueChanged = (prevValue == null && value != null) || (value != null && (prevValue.hashCode() != value.hashCode()));
        boolean sameItem = currentItem != null && prevItem != null && currentItem == prevItem;

        if (valueChanged && sameItem) {

            if (comparator != null) {
                int compare = comparator.compare(value, prevValue);
                if (compare > 0) {
                    background.setBackground(bgIncrease);
                } else if (compare < 0) {
                    background.setBackground(bgDecrease);
                }
            } else {
                background.setBackground(bgChange);
            }

            lblText.setText(String.format("%1.2f", value));

            animation.setFromValue(1);
            animation.setToValue(0);
            animation.setCycleCount(1);
            animation.setAutoReverse(false);
            animation.playFromStart();
        }

        prevValue = value;
        prevItem = currentItem;
    }
}

and this cell factory:

public class FlashingTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
    @Override
    public TableCell<S, T> call(TableColumn<S, T> p) {
        System.out.println("************** CREATING FLASHING TABLE CELL **************");
        FlashingTableCell<S,T> cell = new FlashingTableCell<S,T>(null, Pos.CENTER);
        return cell;
    }
}

And I use it like this:

public class Main extends Application {

    Timer timer = new Timer();

    @Override
    public void start(Stage primaryStage) {

        TableView<InstrumentPrice> table = new TableView<>();
        ObservableList<InstrumentPrice> data = getInitialTableData();
        table.setItems(data);

        TableColumn<InstrumentPrice, String> nameCol = new TableColumn<>("Name");
        nameCol.setCellValueFactory(new PropertyValueFactory<>("instrumentName"));

        TableColumn<InstrumentPrice, Double> openCol = new TableColumn<>("Open");
        openCol.setCellValueFactory(new PropertyValueFactory("open"));
        // Flashing table cell
        FlashingTableCellFactory<tabletest1.InstrumentPrice, Double> ftc2 = new FlashingTableCellFactory<tabletest1.InstrumentPrice, Double>();
        openCol.setCellFactory(ftc2);

        table.getColumns().setAll(nameCol, openCol);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
            // Get and update first item in data array
            InstrumentPrice p = data.get(0);
            p.setOpen(p.getOpen()+1.0);
            data.set(0, p);
            }
        }, 10*1000, 5*1000); // 10 seconds

        StackPane rootLayout = new StackPane(table);
        Scene scene = new Scene(rootLayout, 1000, 300);
        primaryStage.setTitle("Example");
        primaryStage.setScene(scene);
        primaryStage.setX(0);
        primaryStage.setY(0);
        primaryStage.show();
    }

     private ObservableList<InstrumentPrice> getInitialTableData() {
        List list = new ArrayList<InstrumentPrice>();
        list.add(new InstrumentPrice("ABC", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        list.add(new InstrumentPrice("DEF", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        list.add(new InstrumentPrice("GHI", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        list.add(new InstrumentPrice("JKL", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        list.add(new InstrumentPrice("MNO", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        list.add(new InstrumentPrice("PQR", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        list.add(new InstrumentPrice("STU", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        list.add(new InstrumentPrice("VWX", 0.0, 0.0, 0.0, 0.0, 0.0, 0, 0.0, 0, 0.0, 0));
        ObservableList<InstrumentPrice> data = FXCollections.observableList(list);
        return data;
    }


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

I print out some debugging to the console, and here is some of it:

************** CREATING FLASHING TABLE CELL 1299017468
updateItem 1299017468 0 value=0.0 (null)false
updateItem 1299017468 1 value=0.0 (0.0)false
updateItem 1299017468 2 value=0.0 (0.0)false
updateItem 1299017468 3 value=0.0 (0.0)false
updateItem 1299017468 4 value=0.0 (0.0)false
updateItem 1299017468 5 value=0.0 (0.0)false
updateItem 1299017468 6 value=0.0 (0.0)false
updateItem 1299017468 7 value=0.0 (0.0)false
updateItem 1299017468 -1 value=null (0.0)true
************** CREATING FLASHING TABLE CELL 312035237
updateItem 312035237 0 value=0.0 (null)false
updateItem 312035237 -1 value=null (0.0)true
************** CREATING FLASHING TABLE CELL 616040193
updateItem 616040193 0 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 1836880566
updateItem 1836880566 1 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 1717614984
updateItem 1717614984 2 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 114981818
updateItem 114981818 3 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 151715918
updateItem 151715918 4 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 1690114806
updateItem 1690114806 5 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 397552694
updateItem 397552694 6 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 705642570
updateItem 705642570 7 value=0.0 (null)false
************** CREATING FLASHING TABLE CELL 575848070
updateItem 575848070 8 value=null (null)true
************** CREATING FLASHING TABLE CELL 349657094
updateItem 349657094 9 value=null (null)true
************** CREATING FLASHING TABLE CELL 221445895
updateItem 221445895 10 value=null (null)true
************** CREATING FLASHING TABLE CELL 666967962
updateItem 666967962 11 value=null (null)true
updateItem 616040193 -1 value=null (0.0)true
updateItem 1836880566 -1 value=null (0.0)true
updateItem 1717614984 -1 value=null (0.0)true
updateItem 114981818 -1 value=null (0.0)true
updateItem 151715918 -1 value=null (0.0)true
updateItem 1690114806 -1 value=null (0.0)true
updateItem 397552694 -1 value=null (0.0)true
updateItem 705642570 -1 value=null (0.0)true
updateItem 666967962 0 value=1.0 (null)false
updateItem 666967962 0 value=1.0 (1.0)false
updateItem 221445895 1 value=0.0 (null)false
updateItem 221445895 1 value=0.0 (0.0)false
updateItem 349657094 2 value=0.0 (null)false
updateItem 349657094 2 value=0.0 (0.0)false
updateItem 575848070 3 value=0.0 (null)false
updateItem 575848070 3 value=0.0 (0.0)false
updateItem 705642570 4 value=0.0 (0.0)false
updateItem 705642570 4 value=0.0 (0.0)false
updateItem 397552694 5 value=0.0 (0.0)false
updateItem 397552694 5 value=0.0 (0.0)false
updateItem 1690114806 6 value=0.0 (0.0)false
updateItem 1690114806 6 value=0.0 (0.0)false
updateItem 151715918 7 value=0.0 (0.0)false
updateItem 151715918 7 value=0.0 (0.0)false
updateItem 114981818 8 value=null (0.0)true
updateItem 114981818 8 value=null (0.0)true
updateItem 1717614984 9 value=null (0.0)true
updateItem 1717614984 9 value=null (0.0)true
updateItem 1836880566 10 value=null (0.0)true
updateItem 1836880566 10 value=null (0.0)true
updateItem 616040193 11 value=null (0.0)true
updateItem 616040193 11 value=null (0.0)true

There are many problems, but mainly I wonder:

  • Why is the updateItem() method called twice for every update?
  • When I only update row 0, why do I get updateItem() calls for rows 1..n ? I am not resizing the table or anything.
  • The "sameItem" logic doesnt seem to work at all
  • What am I supposed to do when updateItem is called with empty == true?

Somehow I believe that all these questions are related, and that it somehow is caused by my misunderstanding something with the cell factory and cell creation. I have done similar things in many other languages/frameworks, and never been so confused as I am now...

I would appreciate any information about what I have done wrong!

Peter Andersson
  • 1,947
  • 3
  • 28
  • 44
  • whatever you do, you _must not_ update any property in the scenegraph off the fx application thread (as you do in your timertask) – kleopatra Mar 31 '20 at 07:52
  • you have no control whatever when/if/how a cell is re-used - so implementing any animation inside the cell will break sooner or later. Instead, do so outside (in the data or a wrapper around the data), make the table aware of the change and let the cell updates itself based on the state. The grain of salt: didn't really dig into your code, just seeing the Timertask and animation ;) – kleopatra Mar 31 '20 at 07:56
  • I thought TableView was state-of-the-art in JavaFX to do advanced grid stuff (I've seen so many advanced animation demos in JavaFX - though not in a Table), and if I understand correctly you are saying that these features (really basic in my mind) are completely impossible to realize using TableView? That blows my mind! So in which direction should I go to have my basic table with blinking updates then? – Peter Andersson Mar 31 '20 at 08:14
  • @kleopatra You mention the timertask, but I only update the data, not anything in the "scenegraph" (which I don't really know what it is actually). How would I otherwise simulate realtime updates soming from an external source? – Peter Andersson Mar 31 '20 at 09:06
  • _ are completely impossible to realize using TableView_ no, just saying that the cell is the incorrect place to implement it. _but I only update the data, not anything in the "scenegraph"_ data is set to the table which is active in the scenegraph - so in fact you __do__ update a property of the scenegraph (that's the hierarchy of nodes in the ui) off the fx application thread. Please do a bit of research .. fx has extensive concurrency support (f.i. Task), use that instead of the plain java Timer/-Task – kleopatra Mar 31 '20 at 10:19
  • @kleopatra "_so implementing any animation inside the cell will break sooner or later. Instead, do so outside (in the data or a wrapper around the data)_" – with the animation being part of the view, would not the cell be the proper place to implement it? I struggle to see how one could animate some property of the cell from outside the cell without ugly hacks. – Slaw Mar 31 '20 at 12:40
  • @Slaw Exactly my point! Any other way of doing it would be very ineffiecient and really ugly. – Peter Andersson Mar 31 '20 at 13:11
  • @Slaw the stumble stone is the re-use of the cell: as there is no control whatsoever about its when/if/for-which-content it's near to impossible. The other way round: you have to keep some (animation) state that's related to the item anyway. So the question is, where to keep that per-item state. A cell (which is in no controllable way related to an item) is not the place for it. At least the "was-changed" belongs into the data (or very near to it), the animation state could be handled by some wrapper or parallel data structure to the items. – kleopatra Mar 31 '20 at 13:35
  • you (OP and @Slaw) might have a look at what might be an "external" datastructure - Fabian's answer to https://stackoverflow.com/q/52519470/203657 f.i. keeps a map with a per-item animation timer (just for the state change, no animation). – kleopatra Mar 31 '20 at 13:49

1 Answers1

1

Your cell factory must be like this.

sizeBuy.setCellFactory(new Callback<TableColumn<buyOrderBook, Double>, TableCell<buyOrderBook, Double>>() {
    public TableCell<buyOrderBook, Double> call(TableColumn<buyOrderBook, Double> column) {

        Comparator<Double> comparator = new Comparator<Double>() {
                    @Override
                    public int compare(Double o1, Double o2) {
                        return o1.compareTo(o2);
                    }
                };
                
                FlashingTableCell cell = new FlashingTableCell(comparator, Pos.CENTER_RIGHT);

                return label;
    }
});

sizeBuy is my column name. buyOrderBook is my class for that tableview, and Double is the type of value for that column. because iam working on this type app now, so i just improved the FlashingTableCell class and make it doing right, this is FlashingTableCell class full code:

class FlashingTableCell<S, T> extends TableCell<S, T> {

private static final Color INCREASE_HIGHLIGHT_COLOR = Color.rgb(0, 255, 0, 0.4);
private static final Color DECREASE_HIGHLIGHT_COLOR = Color.rgb(255, 0, 0, 0.4);
private static final Color NOTCHANGE_COLOR = Color.rgb(0, 0, 0, 0.0);
private static final Color HIGHLIGHT_COLOR = Color.rgb(255, 255, 0, 0.7);
private static final Duration HIGHLIGHT_TIME = Duration.millis(500);

private final Background bgIncrease = new Background(new BackgroundFill(INCREASE_HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));
private final Background bgDecrease = new Background(new BackgroundFill(DECREASE_HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));
private final Background bgNotChange = new Background(new BackgroundFill(NOTCHANGE_COLOR, CornerRadii.EMPTY, Insets.EMPTY));
private final Background bgChange = new Background(new BackgroundFill(HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));

private final BorderPane background = new BorderPane();
private final LabeledText lblText = new LabeledText(this);
private final FadeTransition animation = new FadeTransition(HIGHLIGHT_TIME, background);

private final StackPane container = new StackPane();

private T prevValue;
private S prevItem;

final private Comparator<T> comparator;


public FlashingTableCell(Comparator<T> comparator, Pos alignment) {
    super();
    this.comparator = comparator;
    lblText.textProperty().bindBidirectional(textProperty());
    this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    lblText.setStyle("-fx-text-fill: lightgray ;");

    setPadding(new Insets(0,10,0,0));
    container.getChildren().addAll(background, lblText);
    container.setAlignment(alignment);

    setGraphic(container);

    animation.setFromValue(1.0);
    animation.setToValue(0);
  // animation.setCycleCount(Timeline.INDEFINITE);
    animation.setCycleCount(0);
    animation.setAutoReverse(false);


    itemProperty().addListener((obs, oldItem, newItem) -> {

       
        if (newItem != null && oldItem != null && getIndex() >= 0  ) {
            int compare = comparator.compare(newItem , oldItem);               
            if (compare > 0) {
                background.setBackground(bgIncrease);
                animation.playFromStart();
            } else if (compare < 0) {
                background.setBackground(bgDecrease);
                animation.playFromStart();
            }


        }
    });
}
@Override
protected void updateItem(T item, boolean empty) {
    super.updateItem(item, empty);
    if(!isEmpty()) {
        lblText.setText(String.format("%1.2f", item));
    }
}

}

i am doing on realtime data using websocket from the exchange. best practice u need using realtime data too,, because like my project right now i impelement this method for the orderbook. and the orderbook sometimes changing like under 200ms.so we can see if this method have much delay or not.

here my project SS : Flashing impelementation on javafx orderbook

h4ckm3-ID
  • 19
  • 3
  • 1
    java naming conventions, please! – kleopatra Jul 15 '20 at 11:03
  • .. and I don't see any animation in your implementation, so what is the relation to the question? – kleopatra Jul 15 '20 at 11:06
  • @kleopatra i just updated my impelementation,, the original class FlashingTableCell actually wrong methods for get newItem and oldItem. listener give update 1 by 1 per index showing. so if using looping to get oldItem manually, its will comparing diffrent value. – h4ckm3-ID Jul 15 '20 at 18:18