Class FlowState<T,C extends Cell<T>>

java.lang.Object
io.github.palexdev.virtualizedfx.flow.FlowState<T,C>

public class FlowState<T,C extends Cell<T>> extends Object
Class used by the FlowManager to represent the state of the viewport at a given time.

The idea is to have an immutable state so each state is a different object, with some exceptional cases when the state doesn't need to be re-computed, so the old object is returned.

This offers information such as:

- The range of items contained in the state, getRange(). Note that sometimes this range doesn't correspond to the needed range of items. To always get a 100% accurate range use the OrientationHelper. An example could be the PaginatedVirtualFlow in case a page has not enough items to be full, still all the needed cells are built anyway and stored in the state.

- The cells in the viewport, mapped like follows itemIndex -> cell

- The position each cell must have in the viewport

- The expected number of cells, getTargetSize()

- The type of event that lead the old state to transition to this new one, see UpdateType

- A flag to check if new cells were created or some were deleted, haveCellsChanged(). This is used by VirtualFlowSkin since we want to update the viewport children only when the cells changed.

- A flag specifically for the PaginatedVirtualFlow to indicate whether the state also contains some cells that are hidden in the viewport, anyHidden()

This also contains a particular global state, EMPTY, typically used to indicate that the viewport is empty, and no state can be created.
  • Field Details

    • EMPTY

      public static final FlowState EMPTY
  • Constructor Details

    • FlowState

      public FlowState(VirtualFlow<T,C> virtualFlow, io.github.palexdev.mfxcore.base.beans.range.IntegerRange range)
  • Method Details

    • transition

      public FlowState<T,C> transition(io.github.palexdev.mfxcore.base.beans.range.IntegerRange newRange)
      This is responsible for transitioning this state to a new one given the new range of items.

      If the given range is equal to getRange() then exits and returns itself.

      The first operation is to set the update type to UpdateType.SCROLL.

      Then all the common cells between getRange() and the given range are added to the new state, no update on them since they are already valid.

      ThisRange: [0, 10]. NewRange: [3, 13]. ValidCells: [3, 10]

      For every missing index cells are removed from this state, updated both in item and index, then added to the new state.

      So in the above example, cells [0, 2] are removed from this state, updated, then added to the new state as [11, 13]

      In some cases (especially for the PaginatedVirtualFlow) it may happen that some cells are still present in the old state, for this reason a check makes sure that they are properly disposed and removed.

      Parameters:
      newRange - the new state's range of items
      Returns:
      the new state
    • transition

      public FlowState<T,C> transition(List<io.github.palexdev.mfxcore.utils.fx.ListChangeHelper.Change> changes)
      This is responsible for transitioning this state to a new one given a series of changes occurred in the items list.

      Note that this is made to work with ListChangeHelper and ListChangeHelper.Change.

      Why? Tl;dr: For performance reasons.

      In JavaFX when multiple changes occur in the list you have to process them one by one, typically in a while loop. This is not great for a virtual flow, which needs to be super efficient, the less are the updates on the viewport the better the performance. ListChangeHelper helps with this by processing all the changes in one big ListChangeHelper.Change bean. Note that since the change is only one, indexes may differ from the JavaFX's ones (during change processing). Why still a list of changes? Well, the JavaFX's API documentation about list change listeners....sucks, and I'm not entirely sure if it will always be a single change, for this reason it is designed to return a list of changes, BUT, it should always be one.

      For each change in the list creates a new state from the previous one, starting by this one, using transition(Change). Once the new state has been computed, changes the update type to UpdateType.CHANGE and copies the positions Set too (for reusable positions)
      Parameters:
      changes - the list of ListChangeHelper.Changes processed by the ListChangeHelper
      Returns:
      the new state
    • transition

      protected FlowState<T,C> transition(io.github.palexdev.mfxcore.utils.fx.ListChangeHelper.Change change)
      This is responsible for processing a single ListChangeHelper.Change bean and produce a new state according to the change's ListChangeHelper.ChangeType.

      In the following lines I'm going to document each type.

      PERMUTATION The permutation case while not being the most complicated can still be considered a bit heavy to compute since cells keep their index but all their items must be updated, so the performance is totally dependent on the cell's Cell.updateItem(Object) implementation.

      REPLACE The algorithm for replacements gets a Deque of the cells keySet, then from the first index to the last one. In this loop it checks if the at the "i" index no change occurred, in this case the cell is removed from the old state and copied as is in the new state.

      If a change occurred, an index is extracted from the deque and at this point two things can happen:

      If the index is null it means that a new cell need to be created

      Otherwise a cell is extracted from the old state, its item updated

      Then the cell index is updated and the cell moved to the new state

      Last but not least we check if there are any remaining cells in the old state. In such case those are disposed, removed from the old state, and also the positions Set is invalidated

      ADDITION The computation in case of added items to the list is complex and a bit heavy on performance. There are several things to consider, and it was hard to find a generic algorithm that would correctly compute the new state in all possible situations, included exceptional cases and for PaginatedVirtualFlow too.

      The simplest of these cases is when changes occur after the displayed range of items, the old state will be returned. In all the other cases the computation for the new state can begin.

      The first step is to get a series of useful information such as:

      - The index of the first item to display, getFirst()

      - The index of the last item to display, getLast() for PaginatedVirtualFlow and getLastAvailable() for VirtualFlow

      At this point the computation begins. The new algorithm uses "mappings" to process how old cells will be used in the new state, see also FlowMapping

      There are three types of mappings:

      The first mappings we do are the ones for valid cells, those that come before the index at which the change occurred. The built mappings are of type FlowMapping.ValidMapping, cells are moved as they are to the new state

      Then we get the cells keySet as a Deque with getKeysDeque() and we start checking for partial updates and full updates.

      For each index in the deque (until the new state doesn't need any more cells) we have three checks:

      - We check if the extracted index has already been mapped before, in this case we go to the next iteration

      - We check that the index is not null (see Deque.poll()) and that the expected new index is in the range of the new state and that it is not one of the indexes at which a change occurred. If all the conditions are met a FlowMapping.PartialMapping is built, this will extract the cell from the old state, update its index and then add it to the new state

      - In case the previous conditions were not met there are yet two cases to distinguish. If the inner conditions were not met then there are cells that can be reused, if the index was null, there are no cells that can be reused. In any of these two cases a FlowMapping.FullMapping is built. The mapping will then decide if a new cell is needed, or an old one must be updated.

      Once the mappings have been created they are "executed" by calling FlowMapping.manage(FlowState, FlowState) for each of them.

      Last steps depend on the flow implementation. For VirtualFlow we check if the old range and the new range are not the same. When an addition occur, the range is sure to not change exception for specific cases, for which the positions Set is invalidated. For PaginatedVirtualFlow we ensure that no cells are left in the old state by moving them to the new state (hidden cells for example)

      REMOVAL The computation in case of removed items from the list is complex and a bit heavy on performance. There are several things to consider, and it was hard to find a generic algorithm that would correctly compute the new state in all possible situations, included exceptional cases.

      The simplest of these cases is when changes occur after the displayed range of items, the old state will be returned. In all the other cases the computation for the new state can begin.

      The first step is to separate those cells that only require a partial update (only index) from the others. First we get a Set of indexes from the state's range using IntegerRange.expandRangeToSet(IntegerRange), then we remove from the Set all the indexes at which the removal occurred. Before looping on these indexes we also convert the ListChangeHelper.Change.getIndexes() to an array of primitives.

      In the loop we extract the cell at index "i" and to update its index we must first compute the shift. We do this by using binary search, Collections.binarySearch(List, Object). The new index will be: int newIndex = index - findShift(list, index), see findShift(List, int).

      If the new index is below the range min the update is skipped and the loop goes to the next index, otherwise, the index is updated and the cell moved to the new state.

      The next step is to update those cells which need a full update (both item and index). First we compute their indexes by expanding the new state's range to a Set and then removing from it all the cells that have already been added to the new state (which means they have been updated already, also keep in mind that we always operate on indexes so newState.cells.keySet()).

      Now a Deque is built on the remaining cells in the old state (cells.keySet() again) and the update can begin. We loop over the previous built Set of indexes:

      1) We get the item at index i

      2) We get one of the indexes from the deque as Deque.removeFirst()

      3) We remove the cell at that index from the old state

      4) We update the cell with the index i (from the loop) and the previously extracted item

      5) The cell is added to the new state

      Last but not least we check if any cells are still available in the old state. These need to be disposed and removed from the positions Set. Note that in such case we also need to indicate that the viewport needs to update its children, as always just by setting haveCellsChanged() to true.

      Parameters:
      change - the ListChangeHelper.Change to process which eventually will lead to the new state
      Returns:
      a new ViewportState object or in exceptional cases the old state
    • computePositions

      public Set<Double> computePositions()
      This is responsible for computing the positions Set which will be used by the OrientationHelper to correctly position the cells in the viewport.

      There are two cases in which the method won't execute:

      1) This is empty

      2) The virtual flow using this state is instance of PaginatedVirtualFlow, in this case computePaginatedPositions() is used instead.

      This is rather complex as this also takes into account some exceptional cases which otherwise would lead to cells being positioned outside the viewport.

      Long story short. The way the OrientationHelper works is that the viewport cannot "scroll" beyond VirtualFlow.getCellSize() and the virtual flow always has one cell of overscan/buffer whatever you want to call it. This means that when you reach the end of the viewport you always have that one cell of overscan that needs to be positioned above all the others, because of course you cannot add anything more at the end. This issue is rather problematic and lead to this solution. The state is also responsible for computing the cells' position as it depends on many factors such as the current range/position of the virtual flow, the type of the state (see UpdateType)

      I'm going to try to explain the functioning:

      First we compute some variables that will be needed later for the layout computation such as:

      - The cells' size

      - The first and last indexes

      - And a flag to indicate whether special adjustments are needed, named "adjust" Just to make it more clear, this is what determines the value of this flag: last > virtualFlow.getItems().size() - 1 && isViewportFull()

      Then we have two separate cases.

      1) The first one is pretty specific, it leads to the layout computation only if the adjust flag is false, the type of state is UpdateType.CHANGE and the positions Set size is greater or equal to the getTargetSize().

      In this case we can use the old positions since some cell may have changed, in index, item or both, but the positions (not considering which cell has the specific position) remain the same.

      2) The second case is where the real layout computation happens. We start from the bottom, so: bottom = (cells.size() - 1) * cellSize. This value is adjusted if the adjust flag is true as bottom -= cellSize.

      At this point we iterate from the end of the range to the start (so reverse order), each cell is put in the positions Set with the current bottom position, updated at each iteration as follows bottom -= cellSize.

    • computePaginatedPositions

      public Set<Double> computePaginatedPositions()
      This is the implementation of computePositions() exclusively for PaginatedVirtualFlows.

      This is much simpler as there is no "free" scrolling, all cells will have a precise position at any time in the page.

      First we clear the positions Set to ensure there's no garbage in it.

      Then we get a series of important parameters, such as:

      - The cells' size

      - The first and last visible cells

      At this point the computation can begin. This method differs in this from computePositions() because it positions cells from top to bottom.

      Another crucial difference is that we must ensure that only the needed cells will be visible. Let's suppose we want to show 5 cells per page but because of the number of items, the last page can show only 2 items. The other 3 cells are not removed from the viewport, but they are hidden and not laid out.

    • findShift

      protected int findShift(List<Integer> indexes, int index)
      Given an ordered list of indexes and the index to find, returns the index at which resides. If the index is not present, returns the index at which it would be located.
      See Also:
    • addCell

      protected void addCell(int index, C cell)
      Adds the given [index, cell] entry in the cells map.
    • addCells

      protected void addCells(Map<Integer,C> cells)
      Adds all the given cells to this state's cells map.
    • removeLast

      protected C removeLast()
      Retrieves the last cell index with getLastAvailable(), then removes and returns the cell from the cells map.
    • isEmpty

      public boolean isEmpty()
      Returns:
      whether the cells map is empty
    • cellsNum

      public int cellsNum()
      Returns:
      the number of cells in the cells map
    • isViewportFull

      public boolean isViewportFull()
      Returns:
      whether the number of cells is greater or equal to getTargetSize(). Special handling for PaginatedVirtualFlow covered, the cellsNum is computed as the number of visible nodes in the cells map
    • getFirst

      public int getFirst()
    • getLast

      public int getLast()
    • getLastAvailable

      public int getLastAvailable()
      Returns:
      the last cell available in the cells map
    • getKeysDeque

      protected Deque<Integer> getKeysDeque()
      Returns:
      converts the cells keySet to a Deque. For PaginatedVirtualFlow extra care is needed since hidden cells must appear at the end of the deque
    • computeTargetSize

      protected int computeTargetSize(int expectedSize)
      Returns:
      the expected number of items of the state
    • clear

      protected void clear()
      Shortcut to dispose all cells present in this state's cells map and then clear it.
    • getVirtualFlow

      public VirtualFlow<T,C> getVirtualFlow()
      Returns:
      the VirtualFlow instance associated to this state
    • getRange

      public io.github.palexdev.mfxcore.base.beans.range.IntegerRange getRange()
      Returns:
      the range of items displayed
    • getCells

      protected Map<Integer,C> getCells()
      Returns:
      the Map containing the cells mapped by the items' index
    • getCellsUnmodifiable

      public Map<Integer,C> getCellsUnmodifiable()
      Returns:
      getCells() but public and wrapped in an unmodifiable Map
    • getNodes

      public List<Node> getNodes()
      By iterating over all the cells in the state (using Streams) this converts them to a list of Nodes, with Cell.getNode()
    • getPositions

      protected Set<Double> getPositions()
      Returns:
      the Map containing the cells mapped by their position in the viewport
    • getPositionsUnmodifiable

      public Set<Double> getPositionsUnmodifiable()
      Returns:
      getPositions() but public and wrapped in an unmodifiable Map
    • getTargetSize

      public int getTargetSize()
      This is the minimum number of cells for which the viewport can be considered full.
    • getType

      public UpdateType getType()
      Returns:
      the event type that lead to the creation of this state
    • haveCellsChanged

      public boolean haveCellsChanged()
      Returns:
      whether changes occurred that lead to the addition or removal of cells, which means that the viewport must update its children
    • setCellsChanged

      protected void setCellsChanged(boolean cellsChanged)
      See Also:
    • anyHidden

      public boolean anyHidden()
      Returns:
      whether any of the cells in the state have been hidden