Class VFXListManager<T,C extends VFXCell<T>>

java.lang.Object
io.github.palexdev.mfxcore.behavior.BehaviorBase<VFXList<T,C>>
io.github.palexdev.virtualizedfx.list.VFXListManager<T,C>
Direct Known Subclasses:
VFXPaginatedListManager

public class VFXListManager<T,C extends VFXCell<T>> extends io.github.palexdev.mfxcore.behavior.BehaviorBase<VFXList<T,C>>
Default behavior implementation for VFXList. Although, to be precise, and as the name also suggests, this can be considered more like a 'manager' than a behavior. Behaviors typically respond to user input, and then update the component's state. This behavior contains core methods to respond to various properties change in VFXList. All computations here will generate a new VFXListState, if possible, and update the list and the layout (indirectly, call to VFXList.requestViewportLayout()).

By default, manages the following changes:

- geometry changes (width/height changes), onGeometryChanged()

- position changes, onPositionChanged()

- cell factory changes, onCellFactoryChanged()

- items change, onItemsChanged()

- fit to viewport flag changes, onFitToViewportChanged()

- cell size changes, onCellSizeChanged()

- orientation changes, onOrientationChanged()

- spacing changes onSpacingChanged()

Last but not least, some of these computations may need to ensure the current vertical and horizontal positions are correct, so that a valid state can be produced. To achieve this, VFXListHelper.invalidatePos() is called. However, invalidating the positions, also means that the onPositionChanged() method could be potentially triggered, thus generating an unwanted 'middle' state. For this reason a special flag invalidatingPos is set to true before the invalidation, so that the other method will exit immediately. It's reset back to false after the computation or if any of the checks before the actual computation fails.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    protected boolean
     
  • Constructor Summary

    Constructors
    Constructor
    Description
     
  • Method Summary

    Modifier and Type
    Method
    Description
    protected boolean
    Avoids code duplication.
    protected VFXListState<T,C>
    Avoids code duplication.
    protected boolean
    Avoids code duplication.
    protected void
    moveReuseCreateAlgorithm(io.github.palexdev.mfxcore.base.beans.range.IntegerRange range, VFXListState<T,C> newState)
    Avoids code duplication.
    protected void
    This method is responsible for updating the list's state when the VFXList.getCellFactory() changes.
    protected void
    This method is responsible for computing a new state when the VFXList.cellSizeProperty() changes.
    protected void
    The easiest of all changes.
    protected void
    This core method is responsible for ensuring that the viewport always has the right number of cells.
    protected void
    Before describing the operations performed by this method, it's important for the reader to understand the difference between the two changes caught by this method.
    protected void
    This method is responsible for computing a new state when the VFXList.orientationProperty() changes.
    protected void
    This core method is responsible for updating the list's state when the 'main' position changes (vPos for VERTICAL orientation, hPos for HORIZONTAL orientation).
    protected void
    This method is responsible for updating the list's state when the VFXList.spacingProperty() changes.
    protected boolean
    rangeCheck(io.github.palexdev.mfxcore.base.beans.range.IntegerRange range, boolean update, boolean dispose)
    Avoids code duplication.
    protected void
    remainingAlgorithm(io.github.palexdev.mfxcore.base.beans.range.ExcludingIntegerRange eRange, VFXListState<T,C> newState)
    Avoids code duplication.

    Methods inherited from class io.github.palexdev.mfxcore.behavior.BehaviorBase

    dispose, getActions, getNode, init, keyPressed, keyPressed, keyReleased, keyReleased, keyTyped, keyTyped, mouseClicked, mouseClicked, mouseDragged, mouseDragged, mouseEntered, mouseEntered, mouseExited, mouseExited, mouseMoved, mouseMoved, mousePressed, mousePressed, mouseReleased, mouseReleased, register, scroll, scroll, touchMoved, touchMoved, touchPressed, touchPressed, touchReleased, touchReleased, touchStationary, touchStationary

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • invalidatingPos

      protected boolean invalidatingPos
  • Constructor Details

    • VFXListManager

      public VFXListManager(VFXList<T,C> list)
  • Method Details

    • onGeometryChanged

      protected void onGeometryChanged()
      This core method is responsible for ensuring that the viewport always has the right number of cells. This is called every time the list's geometry changes (width/height depending on the orientation), which means that this is also responsible for initialization (when width/height becomes > 0.0).

      After preliminary checks done by listFactorySizeCheck() and rangeCheck(IntegerRange, boolean, boolean), the computation for the new state is delegated to the moveReuseCreateAlgorithm(IntegerRange, VFXListState).

      Note that to compute a valid new state, it is important to also validate the list's positions by invoking VFXListHelper.invalidatePos().
    • onPositionChanged

      protected void onPositionChanged()
      This core method is responsible for updating the list's state when the 'main' position changes (vPos for VERTICAL orientation, hPos for HORIZONTAL orientation). Since the list doesn't use any throttling technique to limit the number of events/changes, and since scrolling can happen very fast, performance here is crucial.

      Immediately exists if: the special flag invalidatingPos is true or the current state is VFXListState.INVALID. Many other computations here need to validate the positions by calling VFXListHelper.invalidatePos(), so that the resulting state is valid. However, invalidating the positions may trigger this method, causing two or more state computations to run at the 'same time'; this behavior must be avoided, and that flag exists specifically for this reason.

      For the sake of performance, this method tries to update only the cells which need it. The computation is divided in two steps:

      0) Prerequisites: the last range (retrieved from the current state), the new range (given by VFXListHelper.range()). Note that if these two ranges are equal or the new range in invalid ([-1, -1]), or the number of elements differ, the method exits. The latter condition essentially means that the change is not a position change but something else, if you think about it, when scrolling the number of items cannot change in any way, if it does, then it's surely something else.

      1) First of all, we check for common indexes. Cells are removed from the old state and copied to the new one without updating, since it's not needed. For cells that are not found in the old state (not in common), the index is added to a queue

      2) Now we assume that the number of indexes in the queue and the number of cells remaining in the old state are equal. This is because a position change is not a geometry change, the number cannot change by just scrolling. Remaining cells are removed one by one from the old state, an index is also removed from the queue, then the cell is updated both in index and item. Finally, it is added to the new state, with the index removed from the queue.

      The list's state is updated, VFXList.update(VFXListState), but most importantly this also calls VFXList.requestViewportLayout(). The reason for this 'forced' layout is that the remaining cells, due to the scroll, can now be higher or lower than their previous index, which means that they need to be repositioned in the viewport.

      Last but not least: this algorithm is very similar to the intersectionAlgorithm() one conceptually. Implementation-wise, there are a few different details that make this method much, much faster. For example, one assumption we can make here is that scrolling will not change the number of items to display, which means that all cells from the current/old state can be reused. Even the checks on some preconditions (like list size, cell size, ranges validity) are avoided. We assume that if any of those are not met, the positions cannot be changed, in other words, this will never be called.
    • onCellFactoryChanged

      protected void onCellFactoryChanged()
      This method is responsible for updating the list's state when the VFXList.getCellFactory() changes. Unfortunately, this is always a costly operation because all cells need to be re-created, and the VFXCellsCache cleaned. In fact, the very first operation done by this method is exactly this, the disposal of the current/old state and the cleaning of the cache. Luckily, this kind of change is likely to not happen very often.

      After preliminary checks done by listFactorySizeCheck() and rangeCheck(IntegerRange, boolean, boolean), the computation for the new state is delegated to the moveReuseCreateAlgorithm(IntegerRange, VFXListState).

      The new state's VFXListState.haveCellsChanged() flag will always be true of course. The great thing about the factory change is that there is no need to invalidate the position.

    • onItemsChanged

      protected void onItemsChanged()
      Before describing the operations performed by this method, it's important for the reader to understand the difference between the two changes caught by this method. VFXList makes use of a ListProperty to store the items to display. The property is essentially the equivalent of this ObjectProperty<ObservableList>. Now, if you are familiar with JavaFX, you probably know that there are two possible changes to listen to: one is changes to ObjectProperty (if the ObservableList instance changes), and the other are changes in the ObservableList instance itself. As you may guess, managing both these changes with a simple ObjectProperty is quite cumbersome, because you need two listeners: one that that catches changes in the list, and another to catch changes to the property. In particular, the latter has the task to add the first listener to the new ObservableList instance.

      And here is where ListProperty comes in handy. By adding an InvalidationListener to this special property we are able to intercept both the type of changes always, even if the ObservableList instance changes, everything is handled automatically.

      Needless to say, we use a Property to store the items to allow the usage of bindings!

      This core method is responsible for updating the list's state when any of the two aforementioned changes happen.

      These kind of updates are the most tricky and expensive. In particular, additions and removals can occur at any position in the list, which means that calculating the new state solely on the indexes is a no-go. It is indeed possible by, in theory, isolating the indexes at which the changes occurred, separating the cells that need only an index update from the ones that actually need a full update. However, such an approach requires a lot of code, is error-prone, and a bit heavy on performance. The new approach implemented here requires changes to the state class as well, VFXListState.

      The computation for the new state is similar to the moveReuseCreateAlgorithm(IntegerRange, VFXListState), but the first step, which tries to identify the common cells, is quite different. You see, as I said before, additions and removals can occur at any place in the list. Picture it with this example:

       
       In list before: 0 1 2 3 4 5
       Add at index 2 these items: 99, 98
       In list after: 0 1 99 98 2 3 4 5
      
       Now let's suppose the range of displayed items is the same: [0, 5] (6 items)
       (I'm going now to write items with the index too, like this Index:Item)
       Items before: [0:0, 1:1, 2:2, 3:3, 4:4, 5:5]
       Items after: [0:0, 1:1, 2:99, 3:98, 4:2, 5:3]
       See? Items 2 and 3 are still there but in a different position (index) Since we assume item updates are more
       expensive than index updates, we must ensure to take those two cells and update them just by index
       
       

      For this reason, cells from the old state are not removed by index, but by item, VFXListState.removeCell(Object). First, we retrieve the item from the list that is now at index i (this index comes from the loop on the range), then we try to remove the cell for this item from the old state. If the cell is found, we update it by index and add it to the new state. Note that the index is also excluded from the range.

      Now that 'common' cells have been properly updated, the remaining items are processed by the remainingAlgorithm(ExcludingIntegerRange, VFXListState).

      Last notes:

      1) This is one of those methods that to produce a valid new state needs to validate the list's positions, so it calls VFXListHelper.invalidatePos()

      2) To make sure the layout is always correct, at the end we always invoke VFXList.requestViewportLayout(). You can guess why from the above example, items 2 and 3 are still in the viewport, but at different indexes, which also means at different layout positions. There is no easy way to detect this, so better safe than sorry, always update the layout.

    • onFitToViewportChanged

      protected void onFitToViewportChanged()
      The easiest of all changes. It's enough to request a viewport layout, VFXList.requestViewportLayout(), and to make sure that the horizontal position is valid, VFXListHelper.invalidatePos().
    • onCellSizeChanged

      protected void onCellSizeChanged()
      This method is responsible for computing a new state when the VFXList.cellSizeProperty() changes.

      After preliminary checks done by listFactorySizeCheck(), the computation for the new state is delegated to the intersectionAlgorithm().

      Note that to compute a valid new state, it is important to also validate the list's positions by invoking VFXListHelper.invalidatePos().

      Note that this will request the layout computation, VFXList.requestViewportLayout(), even if the cells didn't change for obvious reasons.
    • onOrientationChanged

      protected void onOrientationChanged()
      This method is responsible for computing a new state when the VFXList.orientationProperty() changes.

      After preliminary checks done by listFactorySizeCheck(), the computation for the new state is delegated to the intersectionAlgorithm().

      Note that the default behavior resets both the positions to 0.0, as maintaining them doesn't make too much sense. Note that to compute a valid new state, it is important to also validate the list's positions by invoking This will also request the layout computation, VFXList.requestViewportLayout(), even if the cells didn't change.
    • onSpacingChanged

      protected void onSpacingChanged()
      This method is responsible for updating the list's state when the VFXList.spacingProperty() changes.

      After preliminary checks done by listFactorySizeCheck() and rangeCheck(IntegerRange, boolean, boolean), the computation for the new state is delegated to the moveReuseCreateAlgorithm(IntegerRange, VFXListState).

      Note that to compute a valid new state, it is important to also validate the list's positions by invoking VFXListHelper.invalidatePos(). Also, this will request the layout computation, VFXList.requestViewportLayout(), even if the cells didn't change.
    • moveReuseCreateAlgorithm

      protected void moveReuseCreateAlgorithm(io.github.palexdev.mfxcore.base.beans.range.IntegerRange range, VFXListState<T,C> newState)
      Avoids code duplication. Typically used when, while iterating on a range, it's enough to move the cells from the current state to the new state. For indexes which are not found in the current state, a new cell is either taken from the old state, taken from cache or created by the cell factory.

      (The last operations are delegated to the remainingAlgorithm(ExcludingIntegerRange, VFXListState)).

      See Also:
    • intersectionAlgorithm

      protected VFXListState<T,C> intersectionAlgorithm()
      Avoids code duplication. Typically used in situations where the previous range and the new one are likely to be very close, but most importantly, that do not involve any change in the items' list. In such cases, the computation for the new state is divided in two parts:

      0) Prerequisites: the new range [min, max], the excluding range (a helper class to keep track of common cells), the current state, and the intersection between the current state's range and the new range

      1) The intersection allows us to distinguish between cells that can be moved as they are, without any update, from the current state to the new one. For this, it's enough to check that the intersection range is valid, and then a for loop. Common indexes are also excluded from the range!

      2) The remaining indexes are items that are new. Which means that if there are still cells in the current state, they need to be updated (both index and item). Otherwise, new ones are created by the cell factory.

      - See Utils.intersection(io.github.palexdev.mfxcore.base.beans.range.IntegerRange, io.github.palexdev.mfxcore.base.beans.range.IntegerRange): used to find the intersection between two ranges

      - See rangeCheck(IntegerRange, boolean, boolean): used to validate the intersection range, both parameters are false!

      - See remainingAlgorithm(ExcludingIntegerRange, VFXListState): the second part of the algorithm is delegated to this method

      See Also:
      • ExcludingIntegerRange
    • remainingAlgorithm

      protected void remainingAlgorithm(io.github.palexdev.mfxcore.base.beans.range.ExcludingIntegerRange eRange, VFXListState<T,C> newState)
      Avoids code duplication. Typically used to process indexes not found in the current state.

      For any index in the given ExcludingIntegerRange, a cell is needed. Also, it needs to be updated by index and item both. This cell can come from three sources:

      1) from the current state if it's not empty yet. Since the cells are stored in a SequencedMap, one is removed by calling IndexBiMap.StateMapBase.pollFirst().

      2) from the VFXCellsCache if not empty

      3) created by the cell factory

      - See VFXListHelper.indexToCell(int): this handles the second and third cases. If a cell can be taken from the cache, automatically updates its item then returns it. Otherwise, invokes the VFXList.getCellFactory() to create a new one

    • listFactorySizeCheck

      protected boolean listFactorySizeCheck()
      Avoids code duplication. This method checks for three things:

      1) If the list is empty

      2) If the cell factory is null

      3) If the cell size is lesser or equal to 0

      If any of those checks is true: the list's state is set to VFXListState.INVALID, the current state is disposed, the 'invalidatingPos' flag is reset, finally returns false. Otherwise, does nothing and returns true.

      - See CellFactory.canCreate()

      - See VFXList.cellSizeProperty()

      - See disposeCurrent(): for the current state disposal

      Returns:
      whether all the aforementioned checks have passed
    • rangeCheck

      protected boolean rangeCheck(io.github.palexdev.mfxcore.base.beans.range.IntegerRange range, boolean update, boolean dispose)
      Avoids code duplication. Used to check whether the given range is valid, not equal to Utils.INVALID_RANGE.

      When invalid, returns false, but first runs the following operations: disposes the current state (only if the 'dispose' parameter is true), sets the list's state to VFXListState.INVALID (only if the 'update' parameter is true), resets the 'invalidatingPos' flag. Otherwise, does nothing and returns true.

      Last but not least, this is a note for the future on why the method is structured like this. It's crucial for the disposal operation to happen before the list's state is set to VFXListState.INVALID, otherwise the disposal method will fail, since it will then retrieve the empty state instead of the correct one.

      - See disposeCurrent(): for the current state disposal

      Parameters:
      range - the range to check
      update - whether to set the list's state to 'empty' if the range is not valid
      dispose - whether to dispose the current/old state if the range is not valid
      Returns:
      whether the range is valid or not
    • disposeCurrent

      protected boolean disposeCurrent()
      Avoids code duplication. Responsible for disposing the current state if it is not empty.

      - See VFXListState.dispose()

      Returns:
      whether the disposal was done or not