0

At first my question is how to let a newly added row flash in JavaFx, then I went through a lot of questions related to this topic (such as javafx: table row flashing). Most of them are using setRowFactory and override the updateItem method by adding a Timeline animation which change the state of pseudoClass of the row. Below is my code, I am trying to building a FlashControl which can be reused.

public class TableFlashControl<T> {

    private PseudoClass flashHighlight = PseudoClass.getPseudoClass("flash-highlight");
    private List<T> newAdded = new ArrayList<>();
    private boolean isFilterApplied = false;
    private boolean isSorted = false;

    public void setIsFilterApplied(boolean isFilterApplied) {
        this.isFilterApplied = isFilterApplied;
    }

    public void add(TableView<T> table){
        ListChangeListener<T> change = c -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    List<? extends T> added = c.getAddedSubList();
                    T lastAdded = added.get(0);
                    if (!isFilterApplied) {
                        newAdded.add(lastAdded);
                    }
                }
            }
        };
        table.getItems().addListener(change);
        table.setRowFactory(param -> new TableRow<T>() {
            @Override
            protected void updateItem(T item, boolean empty) {
                super.updateItem(item, empty);
                if (item == null || empty) {
                    return;
                }
                if (newAdded.contains(item)) {
                    if (isSorted) {
                        new Thread(()->{
                            Timeline flasher = new Timeline(
                                    new KeyFrame(Duration.seconds(0.4), e -> pseudoClassStateChanged(flashHighlight, true)),
                                    new KeyFrame(Duration.seconds(0.8), e -> pseudoClassStateChanged(flashHighlight, false))
                            );
                            flasher.setCycleCount(2);
                            flasher.play();
                        }).start();
                        if (item == newAdded.get(0)) {
                            newAdded.clear();
                            isSorted = false;
                        }

                    }else{
                        if(item == newAdded.get(0)){
                            isSorted = true;
                        }

                    }
                }

            }
        });
    }
}

Here ListChangeListener is associated with table.getItems() which helps me to record the newly inserted row.

It is possible to add multiple rows within one operation which means newAdded.size() can be larger than 1. What's more, rows are inserted from the top of the tableView(because I sort it with the Number.)

In tableView, not all rows are visible and updateItem methods only update those visible rows. My problem comes when these two situations happen(see below).

The first scenario

The first scenario

In first scenario, only 4 rows are visible, if user inserts 5 rows within one time, I cannot record the last row update(the updateItem won't be called for the new_row_5). Thereby, I cannot clear newAdded list (by doing newAdded.clear())

The second scenario

The second scenario

In the second scenario, only 4 rows are visible again. However, there are invisible rows both at top and bottom of the visible rows. If user inserts 2 rows, one will be visible and the other will be invisible. In my case, new_row_2 will flash while new_row_1 remains invisible. If user scrolls up the tableView when new_row_2 is flashing, he will see new_row_2 is flashing while new_row_1 is not which is really weird.

I also want to know if there is any way to find the number of visible rows.

I am still new to JavaFx and I don't know if this method is good or not. I hope someone can help me fix my problems. Thanks a lot!

Community
  • 1
  • 1
ZanderXu
  • 1
  • 3
  • Slightly OT, but why are you wrapping the `Timeline` in a thread? The new thread will simply start the timeline and exit immediately. You can do this perfectly well on the FX Application Thread. – James_D Nov 11 '16 at 13:19
  • @James_D Sorry, that was my fault...I should remove the new thread.. – ZanderXu Nov 14 '16 at 01:18

1 Answers1

2

Your approach doesn't seem like a clean way to do this. The animation depends on the TableRow the item is positioned in and does not seem to support multiple animations happening at the same time. Furthermore it relies on the equals method of the item class not being overridden and on the user not adding a item multiple times to the TableView. Also you potentially create a large number of Timelines (not necessary to start them from a seperate thread btw, since Timeline.play() does not block).

It's better to make the animation depend on the indices. Also keeping track of the TableRows created allows you to access existing cells, should they be be assigned a index that needs to be animated. Also you could handle the animations using a single AnimationTimer by storing the data in a suitable data structure.

Also it would IMHO be most convenient to use the rowFactory class to implement this logic.

The following example makes the rows flash whether they are on-screen or not.

public class FlashTableRowFactory<T> implements Callback<TableView<T>, TableRow<T>> {

    private final static PseudoClass FLASH_HIGHLIGHT = PseudoClass.getPseudoClass("flash-highlight");

    public FlashTableRowFactory(TableView<T> tableView) {
        tableView.getItems().addListener((ListChangeListener.Change<? extends T> c) -> {
            while (c.next()) {
                if (c.wasPermutated()) {
                    int from = c.getFrom();
                    int to = c.getTo();
                    permutationUpdate(scheduledTasks, c, from, to);
                    permutationUpdate(unscheduledTasks, c, from, to);
                }
                if (c.wasReplaced()) {
                    addRange(c.getFrom(), c.getTo());
                } else if (c.wasRemoved()) {
                    int from = c.getFrom();
                    int removed = c.getRemovedSize();
                    removeRange(scheduledTasks, from, from + removed);
                    removeRange(unscheduledTasks, from, from + removed);
                    modifyIndices(unscheduledTasks, from, -removed);
                    modifyIndices(scheduledTasks, from, -removed);
                } else if (c.wasAdded()) {
                    int from = c.getFrom();
                    int to = c.getTo();
                    modifyIndices(unscheduledTasks, from, to - from);
                    modifyIndices(scheduledTasks, from, to - from);
                    addRange(from, to);
                }
            }

            // remove all flashTasks that are older than the youngest for a
            // given index
            Set<Integer> indices = new HashSet<>();
            removeDuplicates(unscheduledTasks, indices);
            removeDuplicates(scheduledTasks, indices);

            flashingIndices.clear();
            updateFlash(lastUpdate);
            refreshFlash();
            if (!unscheduledTasks.isEmpty()) {
                flasher.start();
            }
        });
        this.tableView = tableView;
    }

    private static void removeDuplicates(List<FlashTask> list, Set<Integer> found) {
        for (Iterator<FlashTask> iterator = list.iterator(); iterator.hasNext();) {
            FlashTask next = iterator.next();
            if (!found.add(next.index)) {
                iterator.remove();
            }
        }
    }

    private static void modifyIndices(List<FlashTask> list, int minModify, int by) {
        for (FlashTask task : list) {
            if (task.index >= minModify) {
                task.index += by;
            }
        }
    }

    private void addRange(int index, int to) {
        for (; index < to; index++) {
            unscheduledTasks.add(new FlashTask(index));
        }
    }

    private static void removeRange(List<FlashTask> list, int from, int to) {
        for (Iterator<FlashTask> iterator = list.iterator(); iterator.hasNext();) {
            FlashTask next = iterator.next();
            if (next.index >= from && next.index < to) {
                iterator.remove();
            }
        }
    }

    private static void permutationUpdate(List<FlashTask> list, ListChangeListener.Change<?> c, int from, int to) {
        for (FlashTask task : list) {
            if (task.index < to && task.index >= from) {
                task.index = c.getPermutation(task.index);
            }
        }
    }

    // set of item indices that should flash
    private final Set<Integer> flashingIndices = new HashSet<>();

    // references to every row created by this factory
    private final List<SoftReference<TableRow<T>>> rows = new LinkedList<>();

    // tasks waiting to be scheduled
    private final List<FlashTask> unscheduledTasks = new LinkedList<>();

    // tasks currently being animated
    private final List<FlashTask> scheduledTasks = new LinkedList<>();

    private static class FlashTask {

        int index;
        long schedulingTime;

        public FlashTask(int index) {
            this.index = index;
        }

    }

    private final TableView<T> tableView;
    private long lastUpdate;

    /**
     * Updates flashingIndices set
     * @param now the current timestamp
     * @return true if the set has been modified, otherwise false.
     */
    private boolean updateFlash(long now) {
        boolean modified = false;
        for (Iterator<FlashTask> iterator = scheduledTasks.iterator(); iterator.hasNext();) {
            FlashTask next = iterator.next();

            // running time in seconds
            double runningTime = (now - next.schedulingTime) / (1000d * 1000d * 1000d);

            // slows down the animation for demonstration
            final double animationSpeed = 0.1;

            if (runningTime < 0.4 / animationSpeed) {
                // no need to handle tasks that run for less than 0.4 seconds
                break;
            } else if (runningTime > 1.6 / animationSpeed) {
                // end of task reached
                iterator.remove();
                modified |= flashingIndices.remove(next.index);
            } else if (runningTime > 0.8 / animationSpeed && runningTime < 1.2 / animationSpeed) {
                // second "inactive" interval during animation
                modified |= flashingIndices.remove(next.index);
            } else {
                // activate otherwise
                modified |= flashingIndices.add(next.index);
            }
        }
        return modified;
    }

    private final AnimationTimer flasher = new AnimationTimer() {

        @Override
        public void handle(long now) {
            lastUpdate = now;

            // activate waiting flash tasks
            for (FlashTask task : unscheduledTasks) {
                task.schedulingTime = now;
            }
            scheduledTasks.addAll(unscheduledTasks);
            unscheduledTasks.clear();

            if (updateFlash(now)) {
                refreshFlash();
                if (scheduledTasks.isEmpty()) {
                    // stop, if there are no more tasks
                    stop();
                }
            }
        }

    };

    /**
     * Sets the pseudoclasses of rows based on flashingIndices set
     */
    private void refreshFlash() {
        for (Iterator<SoftReference<TableRow<T>>> iterator = rows.iterator(); iterator.hasNext();) {
            SoftReference<TableRow<T>> next = iterator.next();
            TableRow<T> row = next.get();
            if (row == null) {
                // remove references claimed by garbage collection
                iterator.remove();
            } else {
                row.pseudoClassStateChanged(FLASH_HIGHLIGHT, flashingIndices.contains(row.getIndex()));
            }
        }
    }

    @Override
    public TableRow<T> call(TableView<T> param) {
        if (tableView != param) {
            throw new IllegalArgumentException("This factory can only be used with the table passed to the constructor");
        }
        return new FlashRow();
    }

    private class FlashRow extends TableRow<T> {

        {
            rows.add(new SoftReference<>(this));
        }

        @Override
        public void updateIndex(int i) {
            super.updateIndex(i);

            // update pseudoclass based on flashingIndices set
            pseudoClassStateChanged(FLASH_HIGHLIGHT, flashingIndices.contains(i));
        }

    }

}
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thanks for giving time to answer my question. This method works perfectly:D. It takes time to understand how it works but I really learn a lot. Thanks again! @fabian – ZanderXu Nov 14 '16 at 02:17