Class FlowState<T,C extends Cell<T>>
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()
EMPTY, typically used to indicate that the viewport
is empty, and no state can be created.-
Field Summary
Fields -
Constructor Summary
ConstructorsConstructorDescriptionFlowState(VirtualFlow<T, C> virtualFlow, io.github.palexdev.mfxcore.base.beans.range.IntegerRange range) -
Method Summary
Modifier and TypeMethodDescriptionprotected voidAdds the given [index, cell] entry in the cells map.protected voidAdds all the given cells to this state's cells map.booleanintcellsNum()protected voidclear()Shortcut to dispose all cells present in this state's cells map and then clear it.This is the implementation ofcomputePositions()exclusively forPaginatedVirtualFlows.This is responsible for computing the positionsSetwhich will be used by theOrientationHelperto correctly position the cells in the viewport.protected intcomputeTargetSize(int expectedSize) protected intGiven an ordered list of indexes and the index to find, returns the index at which resides.getCells()intgetFirst()Shortcut forOrientationHelper.firstVisible().intgetLast()Shortcut forOrientationHelper.lastVisible().intgetNodes()By iterating over all the cells in the state (using Streams) this converts them to a list ofNodes, withCell.getNode()io.github.palexdev.mfxcore.base.beans.range.IntegerRangegetRange()intThis is the minimum number of cells for which the viewport can be considered full.getType()booleanbooleanisEmpty()booleanprotected CRetrieves the last cell index withgetLastAvailable(), then removes and returns the cell from the cells map.protected voidsetCellsChanged(boolean cellsChanged) 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.transition(io.github.palexdev.mfxcore.utils.fx.ListChangeHelper.Change change) This is responsible for processing a singleListChangeHelper.Changebean and produce a new state according to the change'sListChangeHelper.ChangeType.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.
-
Field Details
-
EMPTY
-
-
Constructor Details
-
FlowState
public FlowState(VirtualFlow<T, C> virtualFlow, io.github.palexdev.mfxcore.base.beans.range.IntegerRange range)
-
-
Method Details
-
transition
This is responsible for transitioning this state to a new one given the new range of items. If the given range is equal togetRange()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
ListChangeHelperandListChangeHelper.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.
For each change in the list creates a new state from the previous one, starting by this one, usingListChangeHelperhelps with this by processing all the changes in one bigListChangeHelper.Changebean. 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.transition(Change). Once the new state has been computed, changes the update type toUpdateType.CHANGEand copies the positionsSettoo (for reusable positions)- Parameters:
changes- the list ofListChangeHelper.Changes processed by theListChangeHelper- 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 singleListChangeHelper.Changebean and produce a new state according to the change'sListChangeHelper.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'sCell.updateItem(Object)implementation. REPLACE The algorithm for replacements gets aDequeof 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 positionsSetis 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 forPaginatedVirtualFlowtoo. 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()forPaginatedVirtualFlowandgetLastAvailable()forVirtualFlowAt this point the computation begins. The new algorithm uses "mappings" to process how old cells will be used in the new state, see also
There are three types of mappings:FlowMappingThe 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 stateThen we get the cells keySet as a
DequewithgetKeysDeque()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 aFlowMapping.PartialMappingis 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.FullMappingis 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
Last steps depend on the flow implementation. ForFlowMapping.manage(FlowState, FlowState)for each of them.VirtualFlowwe 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 positionsSetis invalidated. ForPaginatedVirtualFlowwe 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 theListChangeHelper.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), seefindShift(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
Dequeis 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 settinghaveCellsChanged()to true.- Parameters:
change- theListChangeHelper.Changeto process which eventually will lead to the new state- Returns:
- a new
ViewportStateobject or in exceptional cases the old state
-
computePositions
This is responsible for computing the positionsSetwhich will be used by theOrientationHelperto 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
This is rather complex as this also takes into account some exceptional cases which otherwise would lead to cells being positioned outside the viewport.PaginatedVirtualFlow, in this casecomputePaginatedPositions()is used instead.Long story short. The way the
I'm going to try to explain the functioning:OrientationHelperworks is that the viewport cannot "scroll" beyondVirtualFlow.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 (seeUpdateType)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:
Then we have two separate cases.last > virtualFlow.getItems().size() - 1 && isViewportFull()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.CHANGEand the positionsSetsize is greater or equal to thegetTargetSize().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 asbottom -= 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
Setwith the current bottom position, updated at each iteration as followsbottom -= cellSize. -
computePaginatedPositions
This is the implementation ofcomputePositions()exclusively forPaginatedVirtualFlows.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 positionsSetto 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
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
Adds the given [index, cell] entry in the cells map. -
addCells
Adds all the given cells to this state's cells map. -
removeLast
Retrieves the last cell index withgetLastAvailable(), 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 forPaginatedVirtualFlowcovered, the cellsNum is computed as the number of visible nodes in the cells map
-
getFirst
public int getFirst()Shortcut forOrientationHelper.firstVisible(). -
getLast
public int getLast()Shortcut forOrientationHelper.lastVisible(). -
getLastAvailable
public int getLastAvailable()- Returns:
- the last cell available in the cells map
-
getKeysDeque
- Returns:
- converts the cells keySet to a
Deque. ForPaginatedVirtualFlowextra 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
- Returns:
- the
VirtualFlowinstance associated to this state
-
getRange
public io.github.palexdev.mfxcore.base.beans.range.IntegerRange getRange()- Returns:
- the range of items displayed
-
getCells
- Returns:
- the Map containing the cells mapped by the items' index
-
getCellsUnmodifiable
- Returns:
getCells()but public and wrapped in an unmodifiable Map
-
getNodes
By iterating over all the cells in the state (using Streams) this converts them to a list ofNodes, withCell.getNode() -
getPositions
- Returns:
- the Map containing the cells mapped by their position in the viewport
-
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
- 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
-