You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
506 lines
23 KiB
C++
506 lines
23 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
#pragma once
|
|
|
|
#include <LyShine/Bus/UiDynamicScrollBoxBus.h>
|
|
#include <LyShine/Bus/UiScrollBoxBus.h>
|
|
#include <LyShine/Bus/UiInitializationBus.h>
|
|
#include <LyShine/Bus/UiTransformBus.h>
|
|
#include <LyShine/Bus/UiElementBus.h>
|
|
#include <LyShine/UiComponentTypes.h>
|
|
|
|
#include <AzCore/Component/Component.h>
|
|
#include <AzCore/Math/Vector2.h>
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//! This component dynamically sets up scrollbox content as a horizontal or vertical list of
|
|
//! elements that are cloned from prototype entities. Only the minimum number of entities are
|
|
//! created for efficient scrolling, and are reused when new elements come into view.
|
|
//! The list can consist of only items, or it can be divided into sections that include a header at
|
|
//! the beginning of each section, followed by items that belong to that section.
|
|
//! The meaning of "element" differs in the public and private interface, mainly for backward
|
|
//! compatibility. In the private interface, "element" refers to a generic entry in the list which
|
|
//! can be of different types. Currently there are two types of elements: headers and items.
|
|
//! In the public interface however, "element" means the same thing as "item" does in the private
|
|
//! interface, and "item" is unused (the public interface does not need to define the concept of a
|
|
//! generic entry in the list.)
|
|
//! Both headers and items can have fixed sizes determined by their corresponding prototype entities,
|
|
//! or they could vary in size. If they vary in size, another option is available to indicate whether
|
|
//! to auto calculate the sizes or request the sizes via a bus interface. There is also the option to
|
|
//! provide an estimated size that will be used until the elements scroll into view and their real
|
|
//! size calculated.
|
|
//! For lists with a large number of elements, it is advisable to use the estimated size as
|
|
//! calculating the sizes of all elements up front could be costly. When elements vary in size,
|
|
//! a cache is maintained and the element sizes are only calculated once.
|
|
class UiDynamicScrollBoxComponent
|
|
: public AZ::Component
|
|
, public UiDynamicScrollBoxBus::Handler
|
|
, public UiScrollBoxNotificationBus::Handler
|
|
, public UiInitializationBus::Handler
|
|
, public UiTransformChangeNotificationBus::Handler
|
|
, public UiElementNotificationBus::Handler
|
|
{
|
|
public: // member functions
|
|
|
|
AZ_COMPONENT(UiDynamicScrollBoxComponent, LyShine::UiDynamicScrollBoxComponentUuid, AZ::Component);
|
|
|
|
UiDynamicScrollBoxComponent();
|
|
~UiDynamicScrollBoxComponent() override;
|
|
|
|
// UiDynamicScrollBoxInterface
|
|
void RefreshContent() override;
|
|
void AddElementsToEnd(int numElementsToAdd, bool scrollToEndIfWasAtEnd) override;
|
|
void RemoveElementsFromFront(int numElementsToRemove) override;
|
|
void ScrollToEnd() override;
|
|
int GetElementIndexOfChild(AZ::EntityId childElement) override;
|
|
int GetSectionIndexOfChild(AZ::EntityId childElement) override;
|
|
AZ::EntityId GetChildAtElementIndex(int index) override;
|
|
AZ::EntityId GetChildAtSectionAndElementIndex(int sectionIndex, int index) override;
|
|
bool GetAutoRefreshOnPostActivate() override;
|
|
void SetAutoRefreshOnPostActivate(bool autoRefresh) override;
|
|
AZ::EntityId GetPrototypeElement() override;
|
|
void SetPrototypeElement(AZ::EntityId prototypeElement) override;
|
|
bool GetElementsVaryInSize() override;
|
|
void SetElementsVaryInSize(bool varyInSize) override;
|
|
bool GetAutoCalculateVariableElementSize() override;
|
|
void SetAutoCalculateVariableElementSize(bool autoCalculateSize) override;
|
|
float GetEstimatedVariableElementSize() override;
|
|
void SetEstimatedVariableElementSize(float estimatedSize) override;
|
|
bool GetSectionsEnabled() override;
|
|
void SetSectionsEnabled(bool enabled) override;
|
|
AZ::EntityId GetPrototypeHeader() override;
|
|
void SetPrototypeHeader(AZ::EntityId prototypeHeader) override;
|
|
bool GetHeadersSticky() override;
|
|
void SetHeadersSticky(bool stickyHeaders) override;
|
|
bool GetHeadersVaryInSize() override;
|
|
void SetHeadersVaryInSize(bool varyInSize) override;
|
|
bool GetAutoCalculateVariableHeaderSize() override;
|
|
void SetAutoCalculateVariableHeaderSize(bool autoCalculateSize) override;
|
|
float GetEstimatedVariableHeaderSize() override;
|
|
void SetEstimatedVariableHeaderSize(float estimatedSize) override;
|
|
// ~UiDynamicScrollBoxInterface
|
|
|
|
// UiScrollBoxNotifications
|
|
void OnScrollOffsetChanging(AZ::Vector2 newScrollOffset) override;
|
|
void OnScrollOffsetChanged(AZ::Vector2 newScrollOffset) override;
|
|
// ~UiScrollBoxNotifications
|
|
|
|
// UiInitializationInterface
|
|
void InGamePostActivate() override;
|
|
// ~UiInitializationInterface
|
|
|
|
// UiTransformChangeNotification
|
|
void OnCanvasSpaceRectChanged(AZ::EntityId entityId, const UiTransformInterface::Rect& oldRect, const UiTransformInterface::Rect& newRect) override;
|
|
// ~UiTransformChangeNotification
|
|
|
|
// UiElementNotifications
|
|
void OnUiElementBeingDestroyed() override;
|
|
// ~UiElementNotifications
|
|
|
|
public: // static member functions
|
|
|
|
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
|
|
{
|
|
provided.push_back(AZ_CRC("UiDynamicScrollBoxService", 0x11112f1a));
|
|
provided.push_back(AZ_CRC("UiDynamicContentService", 0xc5af0b83));
|
|
}
|
|
|
|
static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
|
|
{
|
|
incompatible.push_back(AZ_CRC("UiDynamicContentService", 0xc5af0b83));
|
|
}
|
|
|
|
static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
|
|
{
|
|
required.push_back(AZ_CRC("UiElementService", 0x3dca7ad4));
|
|
required.push_back(AZ_CRC("UiTransformService", 0x3a838e34));
|
|
required.push_back(AZ_CRC("UiScrollBoxService", 0xfdafc904));
|
|
}
|
|
|
|
static void Reflect(AZ::ReflectContext* context);
|
|
|
|
protected: //types
|
|
|
|
// The types of elements that the list may contain
|
|
enum ElementType
|
|
{
|
|
SectionHeader, // element that appears at the start of each section
|
|
Item, // all other elements
|
|
|
|
NumElementTypes
|
|
};
|
|
|
|
// The list can be divided into sections that have a header followed by a list of items
|
|
struct Section
|
|
{
|
|
int m_index; // the section index
|
|
int m_numItems; // the number of items in this section
|
|
int m_headerElementIndex; // the element index of the section header
|
|
};
|
|
|
|
// Used when the list is divided into sections
|
|
struct ElementIndexInfo
|
|
{
|
|
int m_sectionIndex; // the section index that the element belongs to. -1 if the list is not divided into sections
|
|
int m_itemIndexInSection; // the index of the item relative to the section. -1 if the element is a section header
|
|
};
|
|
|
|
// An element that is currently being displayed
|
|
struct DisplayedElement
|
|
{
|
|
AZ::EntityId m_element;
|
|
int m_elementIndex; // the absolute index of the element in the list
|
|
ElementIndexInfo m_indexInfo; // the index info for the element including section index and item index relative to the section
|
|
ElementType m_type;
|
|
};
|
|
|
|
// Used when elements vary in size
|
|
struct CachedElementInfo
|
|
{
|
|
CachedElementInfo();
|
|
|
|
float m_size;
|
|
float m_accumulatedSize;
|
|
};
|
|
|
|
protected: // member functions
|
|
|
|
// AZ::Component
|
|
void Activate() override;
|
|
void Deactivate() override;
|
|
// ~AZ::Component
|
|
|
|
AZ_DISABLE_COPY_MOVE(UiDynamicScrollBoxComponent);
|
|
|
|
// Initialization
|
|
void PrepareListForDisplay();
|
|
|
|
// Get the content entity of the scrollbox
|
|
AZ::Entity* GetContentEntity() const;
|
|
|
|
// Clone a prototype element. The parent defaults to the content entity
|
|
AZ::EntityId ClonePrototypeElement(ElementType elementType, AZ::EntityId parentEntityId = AZ::EntityId()) const;
|
|
|
|
// Check if the specified entity is a prototype element
|
|
bool IsPrototypeElement(AZ::EntityId entityId) const;
|
|
|
|
// Check if all needed prototype elements are valid
|
|
bool AllPrototypeElementsValid() const;
|
|
|
|
// Return whether any of the prototype elements are navigable
|
|
bool AnyPrototypeElementsNavigable() const;
|
|
|
|
// Return whether any element types vary in size
|
|
bool AnyElementTypesHaveVariableSize() const;
|
|
|
|
// Return whether any element types have an estimated size that's greater than 0
|
|
bool AnyElementTypesHaveEstimatedSizes() const;
|
|
|
|
// Return whether all element types have an estimated size that's greater than 0
|
|
bool AllElementTypesHaveEstimatedSizes() const;
|
|
|
|
// Return whether headers should stick to the top or left of the visible area
|
|
bool StickyHeadersEnabled() const;
|
|
|
|
// Resize the content entity to fit all the elements
|
|
void ResizeContentToFitElements();
|
|
|
|
// Resize the content entity to the specified height or width
|
|
void ResizeContentElement(float newSize) const;
|
|
|
|
// Adjust the size of the content entity and the scroll offset by the specified amount.
|
|
// This is used after adding, removing or resizing elements
|
|
void AdjustContentSizeAndScrollOffsetByDelta(float sizeDelta, float scrollDelta) const;
|
|
|
|
// Calculate and cache the size of the element at the specified index
|
|
float CalculateVariableElementSize(int index);
|
|
|
|
// Get the size of the element at the specified index.
|
|
// If the size is in the cache, return that size.
|
|
// Otherwise, if there is an estimated size, return the estimated size.
|
|
// Otherwise, calculate and cache the size
|
|
float GetAndCacheVariableElementSize(int index);
|
|
|
|
// Get the current size of the element at the specified index.
|
|
// The returned size matches the size used to determine the content size.
|
|
// Should only be called when the size is already cached or the element has an estimated size
|
|
float GetVariableElementSize(int index) const;
|
|
|
|
// Get the last element index, before the specified element index, with a known accumulated size
|
|
int GetLastKnownAccumulatedSizeIndex(int index, int numElementsWithUnknownSizeOut[ElementType::NumElementTypes]) const;
|
|
|
|
// Get the offset of the element at the specified index
|
|
float GetElementOffsetAtIndex(int index) const;
|
|
|
|
// Get the offset of the element at the specified index when all element types have a fixed size
|
|
float GetFixedSizeElementOffset(int index) const;
|
|
|
|
// Get the offset of the element at the specified index when some element types have variable size
|
|
float GetVariableSizeElementOffset(int index) const;
|
|
|
|
// Calculate the average element size
|
|
void UpdateAverageElementSize(int numAddedElements, float sizeDelta);
|
|
|
|
// Mark all elements as not displayed
|
|
void ClearDisplayedElements();
|
|
|
|
// Find the displayed element with a matching index
|
|
AZ::EntityId FindDisplayedElementWithIndex(int index) const;
|
|
|
|
// Get the size of the content's parent (visible area)
|
|
float GetVisibleAreaSize() const;
|
|
|
|
// Check if any elements are visible and set the visible content bounds if they are
|
|
bool AreAnyElementsVisible(AZ::Vector2& visibleContentBoundsOut) const;
|
|
|
|
// Update which elements are visible, and set up for display
|
|
void UpdateElementVisibility(bool keepAtEndIfWasAtEnd = false);
|
|
|
|
// Calculate the first and last visible element indices. Elements that have come into view will
|
|
// get their real size calculated if only their estimated size was known.
|
|
// Returns the total change in element size, and the scroll delta needed to keep the previously
|
|
// visible elements at the same position after the content size is changed to accomodate the new
|
|
// element sizes
|
|
void CalculateVisibleElementIndices(bool keepAtEndIfWasAtEnd,
|
|
const AZ::Vector2& visibleContentBounds,
|
|
int& firstVisibleElementIndexOut,
|
|
int& lastVisibleElementIndexOut,
|
|
int& firstDisplayedElementIndexOut,
|
|
int& lastDisplayedElementIndexOut,
|
|
int& firstDisplayedElementIndexWithSizeChangeOut,
|
|
float& totalElementSizeChangeOut,
|
|
float& scrollChangeOut);
|
|
|
|
// Update the header that is currently sticky
|
|
void UpdateStickyHeader(int firstVisibleElementIndex, int lastVisibleElementIndex, float visibleContentBeginning);
|
|
|
|
// Find the first visible header element index excluding the specified header element index
|
|
int FindFirstVisibleHeaderIndex(int firstVisibleElementIndex, int lastVisibleElementIndex, int excludeIndex);
|
|
|
|
// Find the first and last visible element indices when all element types have a fixed size
|
|
void FindVisibleElementIndicesForFixedSizes(const AZ::Vector2& visibleContentBounds,
|
|
int& firstVisibleElementIndexOut,
|
|
int& lastVisibleElementIndexOut) const;
|
|
|
|
// Find the visible element index that should remain in the same position after calculating
|
|
// visible element indices for lists that may not know the sizes of all their elements
|
|
int FindVisibleElementIndexToRemainInPlace(const AZ::Vector2& visibleContentBounds) const;
|
|
|
|
// Add extra elements to the beginning and end for keyboard/gamepad navigation
|
|
void AddExtraElementsForNavigation(int& firstDisplayedElementIndexOut, int& lastDisplayedElementIndexOut) const;
|
|
|
|
// Estimate the index of the first visible element. This is used when elements can vary in size
|
|
int EstimateFirstVisibleElementIndex(const AZ::Vector2& visibleContentBounds) const;
|
|
|
|
// Find the real first visible element index based on an estimated first visible element index.
|
|
// Also returns the bottom or right of the first visible element
|
|
int FindFirstVisibleElementIndex(int estimatedIndex, const AZ::Vector2& visibleContentBounds, float& firstVisibleElementEndOut) const;
|
|
|
|
// Calculate the visible space available before and after the specified visible element index
|
|
void CalculateVisibleSpaceBeforeAndAfterElement(int visibleElementIndex,
|
|
bool keepAtEnd,
|
|
float visibleAreaBeginning,
|
|
float& spaceLeftBeforeOut,
|
|
float& spaceLeftAfterOut) const;
|
|
|
|
// Calculate the first and last visible element indices based on a known visible element index.
|
|
// Elements that have come into view will get their real size calculated if only their estimated
|
|
// size was known. Returns the total change in element size, and the scroll delta needed to keep
|
|
// the top or left of the passed in visible element index at the same position after the content
|
|
// size is changed to accomodate the new element sizes
|
|
void CalculateVisibleElementIndicesFromVisibleElementIndex(int visibleElementIndex,
|
|
const AZ::Vector2& visibleContentBound,
|
|
bool keepAtEnd,
|
|
int& firstVisibleElementIndexOut,
|
|
int& lastVisibleElementIndexOut,
|
|
int& firstDisplayedElementIndexOut,
|
|
int& lastDisplayedElementIndexOut,
|
|
int& firstDisplayedElementIndexWithSizeChangeOut,
|
|
float& totalElementSizeChangeOut,
|
|
float& scrollChangeOut);
|
|
|
|
// Calculate the change in the content's top or left when resizing content with the specified delta.
|
|
// Used to get the scroll delta needed to keep the beginning (top or left) of the content element
|
|
// at the same position after a content size change
|
|
float CalculateContentBeginningDeltaAfterSizeChange(float contentSizeDelta) const;
|
|
|
|
// Calculate the change in the content's bottom or right when resizing content with the specified delta.
|
|
// Used to get the scroll delta needed to keep the end (bottom or right) of the content element
|
|
// at the same position after a content size change
|
|
float CalculateContentEndDeltaAfterSizeChange(float contentSizeDelta) const;
|
|
|
|
// Return whether the list is scrolled to the end
|
|
bool IsScrolledToEnd() const;
|
|
|
|
// Return whether the element at the specified index is being displayed
|
|
bool IsElementDisplayedAtIndex(int index) const;
|
|
|
|
// Get a recycled entity or a new entity for display
|
|
AZ::EntityId GetElementForDisplay(ElementType elementType);
|
|
|
|
// Get an entity to use to auto calculate sizes
|
|
AZ::EntityId GetElementForAutoSizeCalculation(ElementType elementType);
|
|
|
|
// Disable entities used to auto calculate sizes
|
|
void DisableElementsForAutoSizeCalculation() const;
|
|
|
|
// Calculate an element size with the layout cell interface
|
|
float AutoCalculateElementSize(AZ::EntityId elementForAutoSizeCalculation) const;
|
|
|
|
// Set an element's size based on index
|
|
void SizeVariableElementAtIndex(AZ::EntityId element, int index) const;
|
|
|
|
// Set an element's position based on index
|
|
void PositionElementAtIndex(AZ::EntityId element, int index) const;
|
|
|
|
// Set an element's anchors to the top or left
|
|
void SetElementAnchors(AZ::EntityId element) const;
|
|
|
|
// Set an element's offsets based on the specified offset
|
|
void SetElementOffsets(AZ::EntityId element, float offset) const;
|
|
|
|
// Get element type at the specified element index
|
|
ElementType GetElementTypeAtIndex(int index) const;
|
|
|
|
// Get index info of an element from the index of the element in the list
|
|
ElementIndexInfo GetElementIndexInfoFromIndex(int index) const;
|
|
|
|
// Get an index of an element in the list from the index info of that element
|
|
int GetIndexFromElementIndexInfo(const ElementIndexInfo& elementIndexInfo) const;
|
|
|
|
// Get the child of the content entity that contains the specified descendant
|
|
AZ::EntityId GetImmediateContentChildFromDescendant(AZ::EntityId childElement) const;
|
|
|
|
// Used for visibility attribute in reflection
|
|
bool HeadersHaveVariableSizes() const;
|
|
|
|
// Used to check if our item/header prototype elements contain this scrollbox which spawns it.
|
|
// Prototype elements are spawned dynamically, and we need to avoid recursively spawning elements
|
|
bool IsValidPrototype(AZ::EntityId entityId) const;
|
|
|
|
protected: // data
|
|
|
|
//! Whether the list should refresh automatically on post activate
|
|
bool m_autoRefreshOnPostActivate;
|
|
|
|
//! Number of elements by default. Overridden by UiDynamicListDataBus::GetNumElements
|
|
int m_defaultNumElements;
|
|
|
|
//! The prototype element for the items in the list.
|
|
//! Used when sections are enabled
|
|
AZ::EntityId m_itemPrototypeElement;
|
|
|
|
//! Whether items in the list can vary in size (height for vertical lists and width for horizontal lists)
|
|
//! If false, the item size is fixed and is determined by the prototype element
|
|
bool m_variableItemElementSize;
|
|
|
|
//! Whether item sizes should be auto calculated or whether they should be requested.
|
|
//! Applies when items can vary in size
|
|
bool m_autoCalculateItemElementSize;
|
|
|
|
//! An estimated height or width for the item size. If set to greater than 0, this size will be used
|
|
//! until an item goes into view and its real size is calculated.
|
|
//! Applies when items can vary in size
|
|
float m_estimatedItemElementSize;
|
|
|
|
//! Whether the list is divided into sections with headers
|
|
bool m_hasSections;
|
|
|
|
//! Number of sections by default. Overridden by UiDynamicListDataBus::GetNumSections
|
|
int m_defaultNumSections;
|
|
|
|
//! The prototype element for the headers in the list.
|
|
//! Used when sections are enabled
|
|
AZ::EntityId m_headerPrototypeElement;
|
|
|
|
//! Whether headers should stick to the beginning of the visible area
|
|
//! Used when sections are enabled
|
|
bool m_stickyHeaders;
|
|
|
|
//! Whether headers in the list can vary in size (height for vertical lists and width for horizontal lists)
|
|
//! If false, the header size is fixed and is determined by the prototype element
|
|
bool m_variableHeaderElementSize;
|
|
|
|
//! Whether header sizes should be auto calculated or whether they should be requested.
|
|
//! Applies when headers can vary in size
|
|
bool m_autoCalculateHeaderElementSize;
|
|
|
|
//! An estimated height or width for the header size. If set to greater than 0, this size will be used
|
|
//! until a header goes into view and its real size is calculated.
|
|
//! Applies when headers can vary in size
|
|
float m_estimatedHeaderElementSize;
|
|
|
|
//! The entity Ids of the prototype elements
|
|
AZ::EntityId m_prototypeElement[ElementType::NumElementTypes];
|
|
|
|
//! Stores the size of the prototype elements before they are removed from the content element's
|
|
//! child list. Used to calculate the content size and element offsets when there is no variation in element size
|
|
float m_prototypeElementSize[ElementType::NumElementTypes];
|
|
|
|
//! Whether elements in the list can vary in size (height for vertical lists and width for horizontal lists)
|
|
//! If false, the element size is fixed and is determined by the prototype element
|
|
bool m_variableElementSize[ElementType::NumElementTypes];
|
|
|
|
//! Whether element sizes should be auto calculated or whether they should be requested.
|
|
//! Applies when elements can vary in size
|
|
bool m_autoCalculateElementSize[ElementType::NumElementTypes];
|
|
|
|
//! An estimated height or width for the element size. If set to greater than 0, this size will be used
|
|
//! until an element goes into view and its real size is calculated.
|
|
//! Applies when elements can vary in size
|
|
float m_estimatedElementSize[ElementType::NumElementTypes];
|
|
|
|
//! Whether elements in the list are navigable
|
|
bool m_isPrototypeElementNavigable[ElementType::NumElementTypes];
|
|
|
|
//! Average element size of the elements with known sizes
|
|
float m_averageElementSize;
|
|
|
|
//! The number of elements used to calculate the current average
|
|
int m_numElementsUsedForAverage;
|
|
|
|
//! The last calculated offset from the top or left of the content's parent (the visible area) to the top or left of the content.
|
|
//! Used to estimate a first visible element index
|
|
float m_lastCalculatedVisibleContentOffset;
|
|
|
|
//! Whether the list is vertical or horizontal. Determined by the scroll box
|
|
bool m_isVertical;
|
|
|
|
//! A list of currently displayed elements
|
|
AZStd::list<DisplayedElement> m_displayedElements;
|
|
|
|
//! A list of unused entities
|
|
AZStd::list<AZ::EntityId> m_recycledElements[ElementType::NumElementTypes];
|
|
|
|
//! Cloned entities used to auto calculate sizes
|
|
AZ::EntityId m_clonedElementForAutoSizeCalculation[ElementType::NumElementTypes];
|
|
|
|
//! The header that is currently sticky at the top or left of the visible area
|
|
DisplayedElement m_currentStickyHeader;
|
|
|
|
//! First and last element indices that are being displayed (Not necessarily visible in viewport since we add an extra element for gamepad/keyboard navigation purposes)
|
|
int m_firstDisplayedElementIndex;
|
|
int m_lastDisplayedElementIndex;
|
|
|
|
//! First and last element indices that are visible in the viewport
|
|
int m_firstVisibleElementIndex;
|
|
int m_lastVisibleElementIndex;
|
|
|
|
//! Cached element sizes. Used when elements can vary in size
|
|
AZStd::vector<CachedElementInfo> m_cachedElementInfo;
|
|
|
|
//! The virtual number of elements in the list. Includes both headers and items
|
|
int m_numElements;
|
|
|
|
//! List of the sections in the list
|
|
AZStd::vector<Section> m_sections;
|
|
|
|
//! Whether the list has been set up for display
|
|
bool m_listPreparedForDisplay;
|
|
};
|