// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
using System.Diagnostics;
using Unity.Properties;
using UnityEngine.Bindings;
namespace UnityEngine.UIElements
{
// Assuming a ScrollView parent with a flex-direction column.
// The modes follow these rules :
//
// Vertical
// ---------------------
// Require elements with an height, width will stretch.
// If the ScrollView parent is set to flex-direction row the elements height will not stretch.
// How measure works :
// Width is restricted, height is not. content-container is set to overflow: scroll
//
// Horizontal
// ---------------------
// Require elements with a width. If ScrollView is set to flex-grow elements height stretch else they require a height.
// If the ScrollView parent is set to flex-direction row the elements height will stretch.
// How measure works :
// Height is restricted, width is not. content-container is set to overflow: scroll
//
// VerticalAndHorizontal
// ---------------------
// Require elements with an height, width will stretch.
// The difference with the Vertical type is that content will not wrap (white-space has no effect).
// How measure works :
// Nothing is restricted, the content-container will stop shrinking so that all the content fit and scrollers will appear.
// To achieve this content-viewport is set to overflow: scroll and flex-direction: row.
// content-container is set to flex-direction: column, flex-grow: 1 and align-self:flex-start.
//
// This type is more tricky, it requires the content-viewport and content-container to have a different flex-direction.
// "flex-grow:1" is to make elements stretch horizontally.
// "align-self:flex-start" prevent the content-container from shrinking below the content size vertically.
// "overflow:scroll" on the content-viewport and content-container is to not restrict measured elements in any direction.
///
/// Configurations of the to influence the layout of its contents and how scrollbars appear.
///
///
///
/// The default is .
///
/// For more information, refer to [[wiki:UIE-uxml-element-ScrollView|UXML element ScrollView]].
///
public enum ScrollViewMode
{
///
/// Configure for vertical scrolling.
///
///
/// Requires elements with the height property explicitly defined. A ScrollView configured with this mode has the
/// class in its class list.
///
Vertical,
///
/// Configure for horizontal scrolling.
///
///
/// Requires elements with the width property explicitly defined. A ScrollView configured with this mode has the
/// class in its class list.
/// If is set to flex-grow or if it's parent is set to
/// elements height stretch else they require a height.
///
Horizontal,
///
/// Configure for vertical and horizontal scrolling.
///
///
/// Requires elements with the height property explicitly defined. A ScrollView configured with this mode has the
/// class in its class list.
/// The difference with the vertical mode is that content will not wrap.
///
VerticalAndHorizontal
}
///
/// Options for controlling the visibility of scroll bars in the .
///
public enum ScrollerVisibility
{
///
/// Displays a scroll bar only if the content does not fit in the scroll view. Otherwise, hides the scroll bar.
///
Auto,
///
/// The scroll bar is always visible.
///
AlwaysVisible,
///
/// The scroll bar is always hidden.
///
Hidden
}
///
/// Displays its contents inside a scrollable frame. For more information, refer to the [[wiki:UIE-uxml-element-ScrollView|ScrollView]] user manual page.
///
///
/// Both the and the contain a ScrollView that you can manipulate through C# code.
///
///
/// This example creates a ScrollView that contains multiple labels and uses a Button to scroll to a selected label.
///
///
public class ScrollView : VisualElement
{
internal static readonly BindingId horizontalScrollerVisibilityProperty = nameof(horizontalScrollerVisibility);
internal static readonly BindingId verticalScrollerVisibilityProperty = nameof(verticalScrollerVisibility);
internal static readonly BindingId scrollOffsetProperty = nameof(scrollOffset);
internal static readonly BindingId horizontalPageSizeProperty = nameof(horizontalPageSize);
internal static readonly BindingId verticalPageSizeProperty = nameof(verticalPageSize);
internal static readonly BindingId mouseWheelScrollSizeProperty = nameof(mouseWheelScrollSize);
internal static readonly BindingId scrollDecelerationRateProperty = nameof(scrollDecelerationRate);
internal static readonly BindingId elasticityProperty = nameof(elasticity);
internal static readonly BindingId touchScrollBehaviorProperty = nameof(touchScrollBehavior);
internal static readonly BindingId nestedInteractionKindProperty = nameof(nestedInteractionKind);
internal static readonly BindingId modeProperty = nameof(mode);
internal static readonly BindingId elasticAnimationIntervalMsProperty = nameof(elasticAnimationIntervalMs);
[UnityEngine.Internal.ExcludeFromDocs, Serializable]
public new class UxmlSerializedData : VisualElement.UxmlSerializedData
{
[Conditional("UNITY_EDITOR")]
public new static void Register()
{
UxmlDescriptionCache.RegisterType(typeof(UxmlSerializedData), new UxmlAttributeNames[]
{
new (nameof(mode), "mode"),
new (nameof(nestedInteractionKind), "nested-interaction-kind"),
new (nameof(showHorizontal), "show-horizontal-scroller"),
new (nameof(showVertical), "show-vertical-scroller"),
new (nameof(horizontalScrollerVisibility), "horizontal-scroller-visibility"),
new (nameof(verticalScrollerVisibility), "vertical-scroller-visibility"),
new (nameof(horizontalPageSize), "horizontal-page-size"),
new (nameof(verticalPageSize), "vertical-page-size"),
new (nameof(mouseWheelScrollSize), "mouse-wheel-scroll-size"),
new (nameof(touchScrollBehavior), "touch-scroll-type"),
new (nameof(scrollDecelerationRate), "scroll-deceleration-rate"),
new (nameof(elasticity), "elasticity"),
new (nameof(elasticAnimationIntervalMs), "elastic-animation-interval-ms"),
});
}
#pragma warning disable 649
[SerializeField] ScrollViewMode mode;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags mode_UxmlAttributeFlags;
[SerializeField] NestedInteractionKind nestedInteractionKind;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags nestedInteractionKind_UxmlAttributeFlags;
[UxmlAttribute("show-horizontal-scroller"), HideInInspector]
[SerializeField] bool showHorizontal;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags showHorizontal_UxmlAttributeFlags;
[UxmlAttribute("show-vertical-scroller"), HideInInspector]
[SerializeField] bool showVertical;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags showVertical_UxmlAttributeFlags;
[SerializeField] ScrollerVisibility horizontalScrollerVisibility;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags horizontalScrollerVisibility_UxmlAttributeFlags;
[SerializeField] ScrollerVisibility verticalScrollerVisibility;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags verticalScrollerVisibility_UxmlAttributeFlags;
[SerializeField] float horizontalPageSize;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags horizontalPageSize_UxmlAttributeFlags;
[SerializeField] float verticalPageSize;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags verticalPageSize_UxmlAttributeFlags;
[SerializeField] float mouseWheelScrollSize;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags mouseWheelScrollSize_UxmlAttributeFlags;
[UxmlAttribute("touch-scroll-type")]
[SerializeField] TouchScrollBehavior touchScrollBehavior;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags touchScrollBehavior_UxmlAttributeFlags;
[SerializeField] float scrollDecelerationRate;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags scrollDecelerationRate_UxmlAttributeFlags;
[SerializeField] float elasticity;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags elasticity_UxmlAttributeFlags;
[SerializeField] long elasticAnimationIntervalMs;
[SerializeField, UxmlIgnore, HideInInspector] UxmlAttributeFlags elasticAnimationIntervalMs_UxmlAttributeFlags;
#pragma warning restore 649
public override object CreateInstance() => new ScrollView();
public override void Deserialize(object obj)
{
base.Deserialize(obj);
var e = (ScrollView)obj;
if (ShouldWriteAttributeValue(mode_UxmlAttributeFlags))
e.mode = mode;
// Remove once showHorizontal and showVertical are fully deprecated.
#pragma warning disable 618
if (ShouldWriteAttributeValue(horizontalScrollerVisibility_UxmlAttributeFlags))
e.horizontalScrollerVisibility = horizontalScrollerVisibility;
else if (ShouldWriteAttributeValue(showHorizontal_UxmlAttributeFlags))
e.showHorizontal = showHorizontal;
if (ShouldWriteAttributeValue(verticalScrollerVisibility_UxmlAttributeFlags))
e.verticalScrollerVisibility = verticalScrollerVisibility;
else if (ShouldWriteAttributeValue(showVertical_UxmlAttributeFlags))
e.showVertical = showVertical;
#pragma warning restore 618
if (ShouldWriteAttributeValue(nestedInteractionKind_UxmlAttributeFlags))
e.nestedInteractionKind = nestedInteractionKind;
if (ShouldWriteAttributeValue(horizontalPageSize_UxmlAttributeFlags))
e.horizontalPageSize = horizontalPageSize;
if (ShouldWriteAttributeValue(verticalPageSize_UxmlAttributeFlags))
e.verticalPageSize = verticalPageSize;
if (ShouldWriteAttributeValue(mouseWheelScrollSize_UxmlAttributeFlags))
e.mouseWheelScrollSize = mouseWheelScrollSize;
if (ShouldWriteAttributeValue(scrollDecelerationRate_UxmlAttributeFlags))
e.scrollDecelerationRate = scrollDecelerationRate;
if (ShouldWriteAttributeValue(touchScrollBehavior_UxmlAttributeFlags))
e.touchScrollBehavior = touchScrollBehavior;
if (ShouldWriteAttributeValue(elasticity_UxmlAttributeFlags))
e.elasticity = elasticity;
if (ShouldWriteAttributeValue(elasticAnimationIntervalMs_UxmlAttributeFlags))
e.elasticAnimationIntervalMs = elasticAnimationIntervalMs;
}
}
///
/// Instantiates a using the data read from a UXML file.
///
[Obsolete("UxmlFactory is deprecated and will be removed. Use UxmlElementAttribute instead.", false)]
public new class UxmlFactory : UxmlFactory {}
///
/// Defines for the .
///
[Obsolete("UxmlTraits is deprecated and will be removed. Use UxmlElementAttribute instead.", false)]
public new class UxmlTraits : VisualElement.UxmlTraits
{
UxmlEnumAttributeDescription m_ScrollViewMode = new UxmlEnumAttributeDescription
{ name = "mode", defaultValue = ScrollViewMode.Vertical };
UxmlEnumAttributeDescription m_NestedInteractionKind = new UxmlEnumAttributeDescription
{ name = "nested-interaction-kind", defaultValue = NestedInteractionKind.Default };
UxmlBoolAttributeDescription m_ShowHorizontal = new UxmlBoolAttributeDescription
{ name = "show-horizontal-scroller" };
UxmlBoolAttributeDescription m_ShowVertical = new UxmlBoolAttributeDescription
{ name = "show-vertical-scroller" };
UxmlEnumAttributeDescription m_HorizontalScrollerVisibility = new UxmlEnumAttributeDescription
{ name = "horizontal-scroller-visibility"};
UxmlEnumAttributeDescription m_VerticalScrollerVisibility = new UxmlEnumAttributeDescription
{ name = "vertical-scroller-visibility" };
UxmlFloatAttributeDescription m_HorizontalPageSize = new UxmlFloatAttributeDescription
{ name = "horizontal-page-size", defaultValue = k_UnsetPageSizeValue };
UxmlFloatAttributeDescription m_VerticalPageSize = new UxmlFloatAttributeDescription
{ name = "vertical-page-size", defaultValue = k_UnsetPageSizeValue };
UxmlFloatAttributeDescription m_MouseWheelScrollSize = new UxmlFloatAttributeDescription
{ name = "mouse-wheel-scroll-size", defaultValue = k_MouseWheelScrollSizeDefaultValue };
UxmlEnumAttributeDescription m_TouchScrollBehavior = new UxmlEnumAttributeDescription
{ name = "touch-scroll-type", defaultValue = TouchScrollBehavior.Clamped };
UxmlFloatAttributeDescription m_ScrollDecelerationRate = new UxmlFloatAttributeDescription
{ name = "scroll-deceleration-rate", defaultValue = k_DefaultScrollDecelerationRate };
UxmlFloatAttributeDescription m_Elasticity = new UxmlFloatAttributeDescription
{ name = "elasticity", defaultValue = k_DefaultElasticity };
///
/// Initialize properties using values from the attribute bag.
///
/// The object to initialize.
/// The attribute bag.
/// The creation context; unused.
public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
{
base.Init(ve, bag, cc);
ScrollView scrollView = (ScrollView)ve;
scrollView.mode = m_ScrollViewMode.GetValueFromBag(bag, cc);
// Remove once showHorizontal and showVertical are fully deprecated.
#pragma warning disable 618
var horizontalVisibility = ScrollerVisibility.Auto;
if (m_HorizontalScrollerVisibility.TryGetValueFromBag(bag, cc, ref horizontalVisibility))
scrollView.horizontalScrollerVisibility = horizontalVisibility;
else
scrollView.showHorizontal = m_ShowHorizontal.GetValueFromBag(bag, cc);
var verticalVisibility = ScrollerVisibility.Auto;
if (m_VerticalScrollerVisibility.TryGetValueFromBag(bag, cc, ref verticalVisibility))
scrollView.verticalScrollerVisibility = verticalVisibility;
else
scrollView.showVertical = m_ShowVertical.GetValueFromBag(bag, cc);
#pragma warning restore 618
scrollView.nestedInteractionKind = m_NestedInteractionKind.GetValueFromBag(bag, cc);
scrollView.horizontalPageSize = m_HorizontalPageSize.GetValueFromBag(bag, cc);
scrollView.verticalPageSize = m_VerticalPageSize.GetValueFromBag(bag, cc);
scrollView.mouseWheelScrollSize = m_MouseWheelScrollSize.GetValueFromBag(bag, cc);
scrollView.scrollDecelerationRate = m_ScrollDecelerationRate.GetValueFromBag(bag, cc);
scrollView.touchScrollBehavior = m_TouchScrollBehavior.GetValueFromBag(bag, cc);
scrollView.elasticity = m_Elasticity.GetValueFromBag(bag, cc);
}
}
// ScrollViews can take more than 3 passes to stabilize. This can be the case when a scrollview contains elements with height bound to their width (e.g label with wrapped text).
// Beyond 5 passes, we assume that the layout may never be stabilized then we stop updating the visibility of the scrollers.
private const int k_MaxLocalLayoutPassCount = 5;
private int m_FirstLayoutPass = -1; // The layout pass when the first geometry changed occurred. It may not be layoutPass = 0, which could occur when you have nested ScrollViews.
ScrollerVisibility m_HorizontalScrollerVisibility;
///
/// Specifies whether the horizontal scroll bar is visible.
///
[CreateProperty]
public ScrollerVisibility horizontalScrollerVisibility
{
get { return m_HorizontalScrollerVisibility; }
set
{
var previous = m_HorizontalScrollerVisibility;
m_HorizontalScrollerVisibility = value;
UpdateScrollers(needsHorizontal, needsVertical);
if (previous != m_HorizontalScrollerVisibility)
NotifyPropertyChanged(horizontalScrollerVisibilityProperty);
}
}
ScrollerVisibility m_VerticalScrollerVisibility;
///
/// Specifies whether the vertical scroll bar is visible.
///
[CreateProperty]
public ScrollerVisibility verticalScrollerVisibility
{
get { return m_VerticalScrollerVisibility; }
set
{
var previous = m_VerticalScrollerVisibility;
m_VerticalScrollerVisibility = value;
UpdateScrollers(needsHorizontal, needsVertical);
if (previous != m_VerticalScrollerVisibility)
NotifyPropertyChanged(verticalScrollerVisibilityProperty);
}
}
long m_ElasticAnimationIntervalMs = 16;
///
/// The minimum amount of time, in milliseconds, between executions of elastic spring animation.
///
[CreateProperty]
public long elasticAnimationIntervalMs
{
get { return m_ElasticAnimationIntervalMs; }
set
{
var previous = m_ElasticAnimationIntervalMs;
m_ElasticAnimationIntervalMs = value;
if (previous != m_ElasticAnimationIntervalMs)
{
NotifyPropertyChanged(elasticAnimationIntervalMsProperty);
m_PostPointerUpAnimation = schedule.Execute(PostPointerUpAnimation).Every(m_ElasticAnimationIntervalMs);
}
}
}
///
/// Obsolete. Use instead.
///
[Obsolete("showHorizontal is obsolete. Use horizontalScrollerVisibility instead")]
public bool showHorizontal
{
get => horizontalScrollerVisibility == ScrollerVisibility.AlwaysVisible;
set => m_HorizontalScrollerVisibility = value ? ScrollerVisibility.AlwaysVisible : ScrollerVisibility.Auto;
}
///
/// Obsolete. Use instead.
///
[Obsolete("showVertical is obsolete. Use verticalScrollerVisibility instead")]
public bool showVertical
{
get => verticalScrollerVisibility == ScrollerVisibility.AlwaysVisible;
set => m_VerticalScrollerVisibility = value ? ScrollerVisibility.AlwaysVisible : ScrollerVisibility.Auto;
}
// Case 1297053: ScrollableWidth/Height may contain some numerical imprecisions.
const float k_SizeThreshold = 0.001f;
VisualElement m_AttachedRootVisualContainer;
float m_SingleLineHeight = UIElementsUtility.singleLineHeight;
bool m_SingleLineHeightDirtyFlag;
const string k_SingleLineHeightPropertyName = "--unity-metrics-single_line-height";
const float k_ScrollPageOverlapFactor = 0.1f;
internal const float k_UnsetPageSizeValue = -1.0f;
internal const float k_MouseWheelScrollSizeDefaultValue = 18.0f;
internal const float k_MouseWheelScrollSizeUnset = -1.0f;
internal bool m_MouseWheelScrollSizeIsInline;
internal bool needsHorizontal => mode != ScrollViewMode.Vertical && horizontalScrollerVisibility == ScrollerVisibility.AlwaysVisible || (horizontalScrollerVisibility == ScrollerVisibility.Auto && scrollableWidth > k_SizeThreshold);
internal bool needsVertical
{
[VisibleToOtherModules("UnityEditor.UIBuilderModule")]
get
{
return mode != ScrollViewMode.Horizontal &&
verticalScrollerVisibility == ScrollerVisibility.AlwaysVisible ||
(verticalScrollerVisibility == ScrollerVisibility.Auto && scrollableHeight > k_SizeThreshold);
}
}
internal bool isVerticalScrollDisplayed
{
get
{
return verticalScroller.resolvedStyle.display == DisplayStyle.Flex;
}
}
internal bool isHorizontalScrollDisplayed
{
get
{
return horizontalScroller.resolvedStyle.display == DisplayStyle.Flex;
}
}
[SerializeField, DontCreateProperty]
private Vector2 m_ScrollOffset;
///
/// The current scrolling position.
///
[CreateProperty]
public Vector2 scrollOffset
{
get { return m_ScrollOffset; }
set
{
if (value != m_ScrollOffset)
{
horizontalScroller.value = value.x;
verticalScroller.value = value.y;
// Can't directly use the value's x and y as they might be clamped by the slider.
m_ScrollOffset = new Vector2(horizontalScroller.value, verticalScroller.value);
SaveViewData();
if (panel != null)
{
UpdateScrollers(needsHorizontal, needsVertical);
UpdateContentViewTransform();
}
NotifyPropertyChanged(scrollOffsetProperty);
}
}
}
private float m_HorizontalPageSize;
///
/// This property controls the speed of the horizontal scrolling when using a keyboard or the on-screen scrollbar buttons (arrows and handle), based on the size of the page.
///
///
/// When scrolling, the page size is applied to the offset for each scroll step, so the final offset will be the page size multiplied by the number of steps.
///\\
/// SA: [[UIElements.BaseSlider_1.pageSize]]
///
[CreateProperty]
public float horizontalPageSize
{
get { return m_HorizontalPageSize; }
set
{
var previous = m_HorizontalPageSize;
m_HorizontalPageSize = value;
UpdateHorizontalSliderPageSize();
if (!Mathf.Approximately(previous, m_HorizontalPageSize))
NotifyPropertyChanged(horizontalPageSizeProperty);
}
}
private float m_VerticalPageSize;
///
/// This property controls the speed of the vertical scrolling when using a keyboard or the on-screen scrollbar buttons (arrows and handle), based on the size of the page.
///
///
/// The speed is calculated by multiplying the page size by the specified value. For example, a value of `2` means that each scroll movement covers a distance equal to twice the width of the page.\\
/// SA: [[UIElements.BaseSlider_1.pageSize]]
///
[CreateProperty]
public float verticalPageSize
{
get { return m_VerticalPageSize; }
set
{
var previous = m_VerticalPageSize;
m_VerticalPageSize = value;
UpdateVerticalSliderPageSize();
if (!Mathf.Approximately(previous, m_VerticalPageSize))
NotifyPropertyChanged(verticalPageSizeProperty);
}
}
private float m_MouseWheelScrollSize = k_MouseWheelScrollSizeDefaultValue;
///
/// This property controls the scrolling speed only when using a mouse scroll wheel, based on the size of the page.
///
///
/// This property takes precedence over the @@--unity-metrics-single_line-height@@ USS variable. If both the property and the variable
/// are set, the property value is used.
///
///
/// The following example demonstrates how to use the @@mouseWheelScrollSize@@ property to control the "speed" of a scroll
/// when using the mouse wheel. Notice the difference in feel when selecting different values:
///
///
[CreateProperty]
public float mouseWheelScrollSize
{
get { return m_MouseWheelScrollSize; }
set
{
var previous = m_MouseWheelScrollSize;
if (Math.Abs(m_MouseWheelScrollSize - value) > float.Epsilon)
{
m_MouseWheelScrollSizeIsInline = true;
m_MouseWheelScrollSize = value;
NotifyPropertyChanged(mouseWheelScrollSizeProperty);
}
}
}
internal float scrollableWidth
{
get { return contentContainer.boundingBox.width - contentViewport.layout.width; }
}
internal float scrollableHeight
{
get { return contentContainer.boundingBox.height - contentViewport.layout.height; }
}
// For inertia: how quickly the scrollView stops from moving after PointerUp.
private bool hasInertia => scrollDecelerationRate > 0f;
private static readonly float k_DefaultScrollDecelerationRate = 0.135f;
private float m_ScrollDecelerationRate = k_DefaultScrollDecelerationRate;
private float k_ScaledPixelsPerPointMultiplier = 10.0f;
// Equivalent to 240Hz.
private float k_TouchScrollInertiaBaseTimeInterval = 0.004167f;
///
/// Controls the rate at which the scrolling movement slows after a user scrolls using a touch interaction.
///
///
/// The deceleration rate is the speed reduction per second. A value of 0.5 halves the speed each second. A value of 0 stops the scrolling immediately.
///
[CreateProperty]
public float scrollDecelerationRate
{
get { return m_ScrollDecelerationRate; }
set
{
var previous = m_ScrollDecelerationRate;
m_ScrollDecelerationRate = Mathf.Max(0f, value);
if (!Mathf.Approximately(previous, m_ScrollDecelerationRate))
NotifyPropertyChanged(scrollDecelerationRateProperty);
}
}
// For elastic behavior: how long it takes to go back to original position.
private static readonly float k_DefaultElasticity = 0.1f;
private float m_Elasticity = k_DefaultElasticity;
///
/// The amount of elasticity to use when a user tries to scroll past the boundaries of the scroll view.
///
///
/// Elasticity is only used when is set to Elastic.
///
[CreateProperty]
public float elasticity
{
get { return m_Elasticity;}
set
{
var previous = m_Elasticity;
m_Elasticity = Mathf.Max(0f, value);
if (!Mathf.Approximately(previous, m_Elasticity))
NotifyPropertyChanged(elasticityProperty);
}
}
///
/// The behavior to use when a user tries to scroll past the end of the ScrollView content using a touch interaction.
///
public enum TouchScrollBehavior
{
///
/// The content position can move past the ScrollView boundaries.
///
Unrestricted,
///
/// The content position can overshoot the ScrollView boundaries, but then "snaps" back within them.
///
Elastic,
///
/// The content position is clamped to the ScrollView boundaries.
///
Clamped,
}
private TouchScrollBehavior m_TouchScrollBehavior;
///
/// The behavior to use when a user tries to scroll past the boundaries of the ScrollView content using a touch interaction.
///
[CreateProperty]
public TouchScrollBehavior touchScrollBehavior
{
get { return m_TouchScrollBehavior; }
set
{
var previous = m_TouchScrollBehavior;
m_TouchScrollBehavior = value;
if (m_TouchScrollBehavior == TouchScrollBehavior.Clamped)
{
horizontalScroller.slider.clamped = true;
verticalScroller.slider.clamped = true;
}
else
{
horizontalScroller.slider.clamped = false;
verticalScroller.slider.clamped = false;
}
if (previous != m_TouchScrollBehavior)
NotifyPropertyChanged(touchScrollBehaviorProperty);
}
}
///
/// Options for controlling how nested handles scrolling when reaching
/// the limits of the scrollable area.
///
///
/// This Enum is only relevant when used for a ScrollView that is nested inside another ScrollView.
///
///
/// The following example demonstrates how to use the NestedInteractionKind enum to control the behavior of a nested ScrollView.
///
///
public enum NestedInteractionKind
{
///
/// Automatically selects the behavior according to the context in which the UI runs. For touch input, typically mobile devices,
/// NestedInteractionKind.StopScrolling is used. For scroll wheel input, NestedInteractionKind.ForwardScrolling is used.
///
Default,
///
/// Scrolling capture will remain in the scroll view if it initiated the drag.
///
StopScrolling,
///
/// Scrolling will continue to the parent when no movement is possible in the scrolled direction.
///
ForwardScrolling
}
NestedInteractionKind m_NestedInteractionKind;
///
/// The behavior to use when scrolling reaches limits of a nested .
///
[CreateProperty]
public NestedInteractionKind nestedInteractionKind
{
get => m_NestedInteractionKind;
set
{
var previous = m_NestedInteractionKind;
m_NestedInteractionKind = value;
if (previous != m_NestedInteractionKind)
NotifyPropertyChanged(nestedInteractionKindProperty);
}
}
void OnHorizontalScrollDragElementChanged(GeometryChangedEvent evt)
{
if (evt.oldRect.size == evt.newRect.size)
{
return;
}
UpdateHorizontalSliderPageSize();
}
void OnVerticalScrollDragElementChanged(GeometryChangedEvent evt)
{
if (evt.oldRect.size == evt.newRect.size)
{
return;
}
UpdateVerticalSliderPageSize();
}
void UpdateHorizontalSliderPageSize()
{
var containerWidth = horizontalScroller.resolvedStyle.width;
var horizontalSliderPageSize = m_HorizontalPageSize;
if (containerWidth > 0f)
{
if (Mathf.Approximately(m_HorizontalPageSize, k_UnsetPageSizeValue))
{
var sliderDragElementWidth = horizontalScroller.slider.dragElement.resolvedStyle.width;
horizontalSliderPageSize = sliderDragElementWidth * (1f - k_ScrollPageOverlapFactor);
}
}
if (horizontalSliderPageSize >= 0)
{
horizontalScroller.slider.pageSize = horizontalSliderPageSize;
}
}
void UpdateVerticalSliderPageSize()
{
var containerHeight = verticalScroller.resolvedStyle.height;
var verticalSliderPageSize = m_VerticalPageSize;
if (containerHeight > 0f)
{
if (Mathf.Approximately(m_VerticalPageSize, k_UnsetPageSizeValue))
{
var sliderDragElementHeight = verticalScroller.slider.dragElement.resolvedStyle.height;
verticalSliderPageSize = sliderDragElementHeight * (1f - k_ScrollPageOverlapFactor);
}
}
if (verticalSliderPageSize >= 0)
{
verticalScroller.slider.pageSize = verticalSliderPageSize;
}
}
internal void UpdateContentViewTransform()
{
// Adjust contentContainer's position
var t = contentContainer.resolvedStyle.translate;
var offset = scrollOffset;
if (needsVertical)
offset.y += contentContainer.resolvedStyle.top;
t.x = this.RoundToPanelPixelSize(-offset.x);
t.y = this.RoundToPanelPixelSize(-offset.y);
contentContainer.style.translate = t;
// TODO: Can we get rid of this?
this.IncrementVersion(VersionChangeType.Repaint);
}
///
/// Scroll to a specific child element.
///
/// The child to scroll to.
///
/// This example creates a ScrollView that contains multiple labels. A Button is used to scroll to a selected label.
///
///
public void ScrollTo(VisualElement child)
{
if (child == null)
throw new ArgumentNullException(nameof(child));
if (!contentContainer.Contains(child))
throw new ArgumentException("Cannot scroll to a VisualElement that's not a child of the ScrollView content-container.");
m_Velocity = Vector2.zero;
float yDeltaOffset = 0, xDeltaOffset = 0;
if (scrollableHeight > 0)
{
yDeltaOffset = GetYDeltaOffset(child);
verticalScroller.value = scrollOffset.y + yDeltaOffset;
}
if (scrollableWidth > 0)
{
xDeltaOffset = GetXDeltaOffset(child);
horizontalScroller.value = scrollOffset.x + xDeltaOffset;
}
if (yDeltaOffset == 0 && xDeltaOffset == 0)
return;
UpdateContentViewTransform();
}
private float GetXDeltaOffset(VisualElement child)
{
float xTransform = contentContainer.resolvedStyle.translate.x * -1;
var contentWB = contentViewport.worldBound;
float viewMin = contentWB.xMin + xTransform;
float viewMax = contentWB.xMax + xTransform;
var childWB = child.worldBound;
float childBoundaryMin = childWB.xMin + xTransform;
float childBoundaryMax = childWB.xMax + xTransform;
if ((childBoundaryMin >= viewMin && childBoundaryMax <= viewMax) || float.IsNaN(childBoundaryMin) || float.IsNaN(childBoundaryMax))
return 0;
float deltaDistance = GetDeltaDistance(viewMin, viewMax, childBoundaryMin, childBoundaryMax);
return deltaDistance * horizontalScroller.highValue / scrollableWidth;
}
private float GetYDeltaOffset(VisualElement child)
{
float yTransform = contentContainer.resolvedStyle.translate.y * -1;
var contentWB = contentViewport.worldBound;
float viewMin = contentWB.yMin + yTransform;
float viewMax = contentWB.yMax + yTransform;
var childWB = child.worldBound;
float childBoundaryMin = childWB.yMin + yTransform;
float childBoundaryMax = childWB.yMax + yTransform;
if ((childBoundaryMin >= viewMin && childBoundaryMax <= viewMax) || float.IsNaN(childBoundaryMin) || float.IsNaN(childBoundaryMax))
return 0;
float deltaDistance = GetDeltaDistance(viewMin, viewMax, childBoundaryMin, childBoundaryMax);
return deltaDistance * verticalScroller.highValue / scrollableHeight;
}
private float GetDeltaDistance(float viewMin, float viewMax, float childBoundaryMin, float childBoundaryMax)
{
var viewSize = viewMax - viewMin;
var childSize = childBoundaryMax - childBoundaryMin;
if (childSize > viewSize)
{
if (viewMin > childBoundaryMin && childBoundaryMax > viewMax)
return 0f;
return childBoundaryMin > viewMin ? childBoundaryMin - viewMin : childBoundaryMax - viewMax;
}
float deltaDistance = childBoundaryMax - viewMax;
if (deltaDistance < -1)
{
deltaDistance = childBoundaryMin - viewMin;
}
return deltaDistance;
}
///
/// Represents the visible part of contentContainer.
///
/// It can only be accessed.
public VisualElement contentViewport { get; }
///
/// Horizontal scrollbar.
///
public Scroller horizontalScroller { get; }
///
/// Vertical Scrollbar.
///
public Scroller verticalScroller { get; }
private VisualElement m_ContentContainer;
private VisualElement m_ContentAndVerticalScrollContainer;
private float previousVerticalTouchScrollTimeStamp = 0f;
private float previousHorizontalTouchScrollTimeStamp = 0f;
private float elapsedTimeSinceLastVerticalTouchScroll = 0f;
private float elapsedTimeSinceLastHorizontalTouchScroll = 0f;
///
/// Contains full content, potentially partially visible.
///
public override VisualElement contentContainer // Contains full content, potentially partially visible
{
get { return m_ContentContainer; }
}
///
/// USS class name of elements of this type.
///
public static readonly string ussClassName = "unity-scroll-view";
///
/// USS class name of viewport elements in elements of this type.
///
public static readonly string viewportUssClassName = ussClassName + "__content-viewport";
///
/// USS class name that's added when the Viewport is in horizontal mode.
///
///
public static readonly string horizontalVariantViewportUssClassName = viewportUssClassName + "--horizontal";
///
/// USS class name that's added when the Viewport is in vertical mode.
///
///
public static readonly string verticalVariantViewportUssClassName = viewportUssClassName + "--vertical";
///
/// USS class name that's added when the Viewport is in both horizontal and vertical mode.
///
///
public static readonly string verticalHorizontalVariantViewportUssClassName = viewportUssClassName + "--vertical-horizontal";
///
/// USS class name of content elements in elements of this type.
///
public static readonly string contentAndVerticalScrollUssClassName = ussClassName + "__content-and-vertical-scroll-container";
///
/// USS class name of content elements in elements of this type.
///
public static readonly string contentUssClassName = ussClassName + "__content-container";
///
/// USS class name that's added when the ContentContainer is in horizontal mode.
///
///
public static readonly string horizontalVariantContentUssClassName = contentUssClassName + "--horizontal";
///
/// USS class name that's added when the ContentContainer is in vertical mode.
///
///
public static readonly string verticalVariantContentUssClassName = contentUssClassName + "--vertical";
///
/// USS class name that's added when the ContentContainer is in both horizontal and vertical mode.
///
///
public static readonly string verticalHorizontalVariantContentUssClassName = contentUssClassName + "--vertical-horizontal";
///
/// USS class name of horizontal scrollers in elements of this type.
///
public static readonly string hScrollerUssClassName = ussClassName + "__horizontal-scroller";
///
/// USS class name of vertical scrollers in elements of this type.
///
public static readonly string vScrollerUssClassName = ussClassName + "__vertical-scroller";
///
/// USS class name that's added when the ScrollView is in horizontal mode.
///
///
public static readonly string horizontalVariantUssClassName = ussClassName + "--horizontal";
///
/// USS class name that's added when the ScrollView is in vertical mode.
///
///
public static readonly string verticalVariantUssClassName = ussClassName + "--vertical";
///
/// USS class name that's added when the ScrollView is in both horizontal and vertical mode.
///
///
public static readonly string verticalHorizontalVariantUssClassName = ussClassName + "--vertical-horizontal";
///
// TODO why does this exist? It is set in all cases...
public static readonly string scrollVariantUssClassName = ussClassName + "--scroll";
///
/// Constructor.
///
public ScrollView() : this(ScrollViewMode.Vertical) {}
///
/// Constructor.
///
public ScrollView(ScrollViewMode scrollViewMode)
{
AddToClassList(ussClassName);
m_ContentAndVerticalScrollContainer = new VisualElement() { name = "unity-content-and-vertical-scroll-container" };
m_ContentAndVerticalScrollContainer.AddToClassList(contentAndVerticalScrollUssClassName);
hierarchy.Add(m_ContentAndVerticalScrollContainer);
contentViewport = new VisualElement() {name = "unity-content-viewport"};
contentViewport.AddToClassList(viewportUssClassName);
contentViewport.RegisterCallback(OnGeometryChanged);
contentViewport.pickingMode = PickingMode.Ignore;
m_ContentAndVerticalScrollContainer.RegisterCallback(OnAttachToPanel);
m_ContentAndVerticalScrollContainer.RegisterCallback(OnDetachFromPanel);
m_ContentAndVerticalScrollContainer.Add(contentViewport);
m_ContentContainer = new VisualElement() {name = "unity-content-container"};
// Content container overflow is set to scroll which clip but we need to disable clipping in this case
// or else absolute elements might not be shown. The viewport is in charge of clipping.
// See case 1247583
m_ContentContainer.disableClipping = true;
m_ContentContainer.RegisterCallback(OnGeometryChanged);
m_ContentContainer.AddToClassList(contentUssClassName);
m_ContentContainer.usageHints = UsageHints.GroupTransform;
contentViewport.Add(m_ContentContainer);
SetScrollViewMode(scrollViewMode);
const int defaultMinScrollValue = 0;
const int defaultMaxScrollValue = int.MaxValue;
horizontalScroller = new Scroller(defaultMinScrollValue, defaultMaxScrollValue,
(value) =>
{
scrollOffset = new Vector2(value, scrollOffset.y);
UpdateContentViewTransform();
}, SliderDirection.Horizontal)
{ viewDataKey = "HorizontalScroller" };
horizontalScroller.AddToClassList(hScrollerUssClassName);
horizontalScroller.style.display = DisplayStyle.None;
hierarchy.Add(horizontalScroller);
verticalScroller = new Scroller(defaultMinScrollValue, defaultMaxScrollValue,
(value) =>
{
scrollOffset = new Vector2(scrollOffset.x, value);
UpdateContentViewTransform();
}, SliderDirection.Vertical)
{ viewDataKey = "VerticalScroller" };
verticalScroller.slider.viewDataRestored += OnVerticalSliderViewDataRestored;
horizontalScroller.slider.viewDataRestored += OnHorizontalSliderViewDataRestored;
horizontalScroller.slider.onSetValueWithoutNotify += OnHorizontalScrollerSetValueWithoutNotify;
verticalScroller.slider.onSetValueWithoutNotify += OnVerticalScrollerSetValueWithoutNotify;
horizontalScroller.slider.clampedDragger.draggingEnded += UpdateElasticBehaviour;
verticalScroller.slider.clampedDragger.draggingEnded += UpdateElasticBehaviour;
horizontalScroller.slider.clampedDragger.acceptClicksIfDisabled = true;
verticalScroller.slider.clampedDragger.acceptClicksIfDisabled = true;
verticalScroller.highButton.acceptClicksIfDisabled = true;
verticalScroller.lowButton.acceptClicksIfDisabled = true;
horizontalScroller.highButton.acceptClicksIfDisabled = true;
horizontalScroller.lowButton.acceptClicksIfDisabled = true;
horizontalScroller.lowButton.AddAction(UpdateElasticBehaviour);
horizontalScroller.highButton.AddAction(UpdateElasticBehaviour);
verticalScroller.lowButton.AddAction(UpdateElasticBehaviour);
verticalScroller.highButton.AddAction(UpdateElasticBehaviour);
verticalScroller.AddToClassList(vScrollerUssClassName);
verticalScroller.style.display = DisplayStyle.None;
m_ContentAndVerticalScrollContainer.Add(verticalScroller);
touchScrollBehavior = TouchScrollBehavior.Clamped;
RegisterCallback(OnScrollWheel, InvokePolicy.IncludeDisabled);
verticalScroller.RegisterCallback(OnScrollersGeometryChanged);
horizontalScroller.RegisterCallback(OnScrollersGeometryChanged);
horizontalPageSize = k_UnsetPageSizeValue;
verticalPageSize = k_UnsetPageSizeValue;
horizontalScroller.slider.dragElement.RegisterCallback(OnHorizontalScrollDragElementChanged);
verticalScroller.slider.dragElement.RegisterCallback(OnVerticalScrollDragElementChanged);
m_CapturedTargetPointerMoveCallback = OnPointerMove;
m_CapturedTargetPointerUpCallback = OnPointerUp;
scrollOffset = Vector2.zero;
m_ContentContainer.receivesHierarchyGeometryChangedEvents = true;
}
private ScrollViewMode m_Mode;
///
/// Controls how the ScrollView allows the user to scroll the contents.
///
///
///
/// The default is .
/// Writing to this property modifies the class list of the ScrollView according to the specified value of
/// . When the value changes, the class list matching the old value is removed and
/// the class list matching the new value is added.
///
[CreateProperty]
public ScrollViewMode mode
{
get => m_Mode;
set
{
var previous = m_Mode;
SetScrollViewMode(value);
if (previous != m_Mode)
NotifyPropertyChanged(modeProperty);
}
}
private void SetScrollViewMode(ScrollViewMode mode)
{
m_Mode = mode;
RemoveFromClassList(verticalVariantUssClassName);
RemoveFromClassList(horizontalVariantUssClassName);
RemoveFromClassList(verticalHorizontalVariantUssClassName);
RemoveFromClassList(scrollVariantUssClassName);
contentContainer.RemoveFromClassList(verticalVariantContentUssClassName);
contentContainer.RemoveFromClassList(horizontalVariantContentUssClassName);
contentContainer.RemoveFromClassList(verticalHorizontalVariantContentUssClassName);
contentViewport.RemoveFromClassList(verticalVariantViewportUssClassName);
contentViewport.RemoveFromClassList(horizontalVariantViewportUssClassName);
contentViewport.RemoveFromClassList(verticalHorizontalVariantViewportUssClassName);
switch (mode)
{
case ScrollViewMode.Vertical:
AddToClassList(scrollVariantUssClassName);
AddToClassList(verticalVariantUssClassName);
contentViewport.AddToClassList(verticalVariantViewportUssClassName);
contentContainer.AddToClassList(verticalVariantContentUssClassName);
break;
case ScrollViewMode.Horizontal:
AddToClassList(scrollVariantUssClassName);
AddToClassList(horizontalVariantUssClassName);
contentViewport.AddToClassList(horizontalVariantViewportUssClassName);
contentContainer.AddToClassList(horizontalVariantContentUssClassName);
break;
case ScrollViewMode.VerticalAndHorizontal:
AddToClassList(scrollVariantUssClassName);
AddToClassList(verticalHorizontalVariantUssClassName);
contentViewport.AddToClassList(verticalHorizontalVariantViewportUssClassName);
contentContainer.AddToClassList(verticalHorizontalVariantContentUssClassName);
break;
}
}
private void OnAttachToPanel(AttachToPanelEvent evt)
{
if (evt.destinationPanel == null)
{
return;
}
m_AttachedRootVisualContainer = GetRootVisualContainer();
m_AttachedRootVisualContainer?.RegisterCallback(OnRootCustomStyleResolved);
MarkSingleLineHeightDirty();
if (evt.destinationPanel.contextType == ContextType.Player)
{
m_ContentAndVerticalScrollContainer.RegisterCallback(OnPointerMove);
contentContainer.RegisterCallback(OnPointerDown, TrickleDown.TrickleDown);
contentContainer.RegisterCallback(OnPointerCancel);
contentContainer.RegisterCallback(OnPointerUp, TrickleDown.TrickleDown);
contentContainer.RegisterCallback(OnPointerCapture);
contentContainer.RegisterCallback(OnPointerCaptureOut);
evt.destinationPanel.visualTree.RegisterCallback(OnRootPointerUp, TrickleDown.TrickleDown);
}
}
private void OnDetachFromPanel(DetachFromPanelEvent evt)
{
m_ScheduledLayoutPassResetItem?.Pause();
ResetLayoutPass();
if (evt.originPanel == null)
{
return;
}
m_AttachedRootVisualContainer?.UnregisterCallback(OnRootCustomStyleResolved);
m_AttachedRootVisualContainer = null;
if (evt.originPanel.contextType == ContextType.Player)
{
m_ContentAndVerticalScrollContainer.UnregisterCallback(OnPointerMove);
contentContainer.UnregisterCallback(OnPointerDown, TrickleDown.TrickleDown);
contentContainer.UnregisterCallback(OnPointerCancel);
contentContainer.UnregisterCallback(OnPointerUp, TrickleDown.TrickleDown);
contentContainer.UnregisterCallback(OnPointerCapture);
contentContainer.UnregisterCallback(OnPointerCaptureOut);
evt.originPanel.visualTree.UnregisterCallback(OnRootPointerUp, TrickleDown.TrickleDown);
}
}
void OnPointerCapture(PointerCaptureEvent evt)
{
m_CapturedTarget = evt.elementTarget;
if (m_CapturedTarget == null)
return;
m_TouchPointerMoveAllowed = true;
m_CapturedTarget.RegisterCallback(m_CapturedTargetPointerMoveCallback);
m_CapturedTarget.RegisterCallback(m_CapturedTargetPointerUpCallback);
}
void OnPointerCaptureOut(PointerCaptureOutEvent evt)
{
ReleaseScrolling(evt.pointerId, evt.target);
if (m_CapturedTarget == null)
return;
m_CapturedTarget.UnregisterCallback(m_CapturedTargetPointerMoveCallback);
m_CapturedTarget.UnregisterCallback(m_CapturedTargetPointerUpCallback);
m_CapturedTarget = null;
}
private void OnGeometryChanged(GeometryChangedEvent evt)
{
// Only affected by dimension changes
if (evt.oldRect.size == evt.newRect.size)
{
return;
}
// Get the initial information on the necessity of the scrollbars
bool needsVerticalCached = needsVertical;
bool needsHorizontalCached = needsHorizontal;
if (m_FirstLayoutPass == -1)
m_FirstLayoutPass = evt.layoutPass;
else
{
// Here, we update the visibility of the scrollbars for only few layout pass.
// Exceeding this limit could suggest that the layout will never be stabilized if we keep showing/hiding the scrollbars.
if ((evt.layoutPass - m_FirstLayoutPass) > k_MaxLocalLayoutPassCount)
{
needsVerticalCached = needsVerticalCached || isVerticalScrollDisplayed;
needsHorizontalCached = needsHorizontalCached || isHorizontalScrollDisplayed;
}
}
UpdateScrollers(needsHorizontalCached, needsVerticalCached);
UpdateContentViewTransform();
ScheduleResetLayoutPass();
}
void OnVerticalSliderViewDataRestored()
{
verticalScroller.highValue = float.IsNaN(scrollableHeight) ? verticalScroller.highValue : scrollableHeight;
UpdateContentViewTransform();
}
void OnHorizontalSliderViewDataRestored()
{
horizontalScroller.highValue = float.IsNaN(scrollableWidth) ? horizontalScroller.highValue : scrollableWidth;
UpdateContentViewTransform();
}
void OnVerticalScrollerSetValueWithoutNotify(float value)
{
m_ScrollOffset = new Vector2(scrollOffset.x, value);
SaveViewData();
}
void OnHorizontalScrollerSetValueWithoutNotify(float value)
{
m_ScrollOffset = new Vector2(value, scrollOffset.y);
SaveViewData();
}
private IVisualElementScheduledItem m_ScheduledLayoutPassResetItem;
void ScheduleResetLayoutPass()
{
// Reset the cached layout pass information in the next frame.
if (m_ScheduledLayoutPassResetItem == null)
{
m_ScheduledLayoutPassResetItem = schedule.Execute(ResetLayoutPass);
}
else
{
m_ScheduledLayoutPassResetItem.Pause();
m_ScheduledLayoutPassResetItem.Resume();
}
}
void ResetLayoutPass()
{
m_FirstLayoutPass = -1;
}
private const float k_VelocityLerpTimeFactor = 10;
internal const float ScrollThresholdSquared = 100;
private Vector2 m_StartPosition;
private Vector2 m_PointerStartPosition;
private Vector2 m_Velocity;
private Vector2 m_SpringBackVelocity;
private Vector2 m_LowBounds;
private Vector2 m_HighBounds;
private float m_LastVelocityLerpTime;
private bool m_StartedMoving;
private bool m_TouchPointerMoveAllowed;
private bool m_TouchStoppedVelocity;
VisualElement m_CapturedTarget;
EventCallback m_CapturedTargetPointerMoveCallback;
EventCallback m_CapturedTargetPointerUpCallback;
// Internal for tests
internal IVisualElementScheduledItem m_PostPointerUpAnimation;
// Compute the new scroll view offset from a pointer delta, taking elasticity into account.
// Low and high limits are the values beyond which the scrollview starts to show resistance to scrolling (elasticity).
// Low and high hard limits are the values beyond which it is infinitely hard to scroll.
// The mapping between the normalized pointer delta and normalized scroll view offset delta in the
// elastic zone is: offsetDelta = 1 - 1 / (pointerDelta + 1)
private static float ComputeElasticOffset(float deltaPointer, float initialScrollOffset, float lowLimit,
float hardLowLimit, float highLimit, float hardHighLimit)
{
// initialScrollOffset should be between hardLowLimit and hardHighLimit.
// Add safety margin to avoid division by zero in code below.
initialScrollOffset = Mathf.Max(initialScrollOffset, hardLowLimit * .95f);
initialScrollOffset = Mathf.Min(initialScrollOffset, hardHighLimit * .95f);
float delta;
float scaleFactor;
if (initialScrollOffset < lowLimit && hardLowLimit < lowLimit)
{
scaleFactor = lowLimit - hardLowLimit;
// Find the current potential energy of current scroll offset
var currentEnergy = (lowLimit - initialScrollOffset) / scaleFactor;
// Find the cursor displacement that was needed to get there.
// Because initialScrollOffset > hardLowLimit, we have currentEnergy < 1
delta = currentEnergy * scaleFactor / (1 - currentEnergy);
// Merge with deltaPointer
delta += deltaPointer;
// Now it is as if the initial offset was at low limit and the pointer delta was delta.
initialScrollOffset = lowLimit;
}
else if (initialScrollOffset > highLimit && hardHighLimit > highLimit)
{
scaleFactor = hardHighLimit - highLimit;
// Find the current potential energy of current scroll offset
var currentEnergy = (initialScrollOffset - highLimit) / scaleFactor;
// Find the cursor displacement that was needed to get there.
// Because initialScrollOffset > hardLowLimit, we have currentEnergy < 1
delta = -1 * currentEnergy * scaleFactor / (1 - currentEnergy);
// Merge with deltaPointer
delta += deltaPointer;
// Now it is as if the initial offset was at high limit and the pointer delta was delta.
initialScrollOffset = highLimit;
}
else
{
delta = deltaPointer;
}
var newOffset = initialScrollOffset - delta;
float direction;
if (newOffset < lowLimit)
{
// Apply elasticity on the portion below lowLimit
delta = lowLimit - newOffset;
initialScrollOffset = lowLimit;
scaleFactor = lowLimit - hardLowLimit;
direction = 1f;
}
else if (newOffset <= highLimit)
{
return newOffset;
}
else
{
// Apply elasticity on the portion beyond highLimit
delta = newOffset - highLimit;
initialScrollOffset = highLimit;
scaleFactor = hardHighLimit - highLimit;
direction = -1f;
}
if (Mathf.Abs(delta) < UIRUtility.k_Epsilon)
{
return initialScrollOffset;
}
// Compute energy given by the pointer displacement
// normalizedDelta = delta / scaleFactor;
// energy = 1 - 1 / (normalizedDelta + 1) = delta / (delta + scaleFactor)
var energy = delta / (delta + scaleFactor);
// Scale energy and use energy to do work on the offset
energy *= scaleFactor;
energy *= direction;
newOffset = initialScrollOffset - energy;
return newOffset;
}
private void ComputeInitialSpringBackVelocity()
{
if (touchScrollBehavior != TouchScrollBehavior.Elastic)
{
m_SpringBackVelocity = Vector2.zero;
return;
}
if (scrollOffset.x < m_LowBounds.x)
{
m_SpringBackVelocity.x = m_LowBounds.x - scrollOffset.x;
}
else if (scrollOffset.x > m_HighBounds.x)
{
m_SpringBackVelocity.x = m_HighBounds.x - scrollOffset.x;
}
else
{
m_SpringBackVelocity.x = 0;
}
if (scrollOffset.y < m_LowBounds.y)
{
m_SpringBackVelocity.y = m_LowBounds.y - scrollOffset.y;
}
else if (scrollOffset.y > m_HighBounds.y)
{
m_SpringBackVelocity.y = m_HighBounds.y - scrollOffset.y;
}
else
{
m_SpringBackVelocity.y = 0;
}
}
private void SpringBack()
{
if (touchScrollBehavior != TouchScrollBehavior.Elastic)
{
m_SpringBackVelocity = Vector2.zero;
return;
}
var newOffset = scrollOffset;
if (newOffset.x < m_LowBounds.x)
{
newOffset.x = Mathf.SmoothDamp(newOffset.x, m_LowBounds.x, ref m_SpringBackVelocity.x, elasticity,
Mathf.Infinity, elapsedTimeSinceLastHorizontalTouchScroll);
if (Mathf.Abs(m_SpringBackVelocity.x) < scaledPixelsPerPoint)
{
m_SpringBackVelocity.x = 0;
}
}
else if (newOffset.x > m_HighBounds.x)
{
newOffset.x = Mathf.SmoothDamp(newOffset.x, m_HighBounds.x, ref m_SpringBackVelocity.x, elasticity,
Mathf.Infinity, elapsedTimeSinceLastHorizontalTouchScroll);
if (Mathf.Abs(m_SpringBackVelocity.x) < scaledPixelsPerPoint)
{
m_SpringBackVelocity.x = 0;
}
}
else
{
m_SpringBackVelocity.x = 0;
}
if (newOffset.y < m_LowBounds.y)
{
newOffset.y = Mathf.SmoothDamp(newOffset.y, m_LowBounds.y, ref m_SpringBackVelocity.y, elasticity,
Mathf.Infinity, elapsedTimeSinceLastVerticalTouchScroll);
if (Mathf.Abs(m_SpringBackVelocity.y) < scaledPixelsPerPoint)
{
m_SpringBackVelocity.y = 0;
}
}
else if (newOffset.y > m_HighBounds.y)
{
newOffset.y = Mathf.SmoothDamp(newOffset.y, m_HighBounds.y, ref m_SpringBackVelocity.y, elasticity,
Mathf.Infinity, elapsedTimeSinceLastVerticalTouchScroll);
if (Mathf.Abs(m_SpringBackVelocity.y) < scaledPixelsPerPoint)
{
m_SpringBackVelocity.y = 0;
}
}
else
{
m_SpringBackVelocity.y = 0;
}
scrollOffset = newOffset;
}
// Internal for tests.
internal void ApplyScrollInertia()
{
if (hasInertia && m_Velocity != Vector2.zero)
{
var additionalOffset = Vector2.zero;
var cumulativeDeltaTimeCovered = 0f;
while (cumulativeDeltaTimeCovered < elapsedTimeSinceLastVerticalTouchScroll)
{
m_Velocity *= Mathf.Pow(scrollDecelerationRate, k_TouchScrollInertiaBaseTimeInterval);
cumulativeDeltaTimeCovered += k_TouchScrollInertiaBaseTimeInterval;
additionalOffset += m_Velocity * k_TouchScrollInertiaBaseTimeInterval;
}
var remainingTimeDifference = elapsedTimeSinceLastVerticalTouchScroll - cumulativeDeltaTimeCovered;
if (remainingTimeDifference > 0 && remainingTimeDifference < k_TouchScrollInertiaBaseTimeInterval)
{
m_Velocity *= Mathf.Pow(scrollDecelerationRate, remainingTimeDifference);
additionalOffset += m_Velocity * remainingTimeDifference;
}
var scaledSpeedLimit = scaledPixelsPerPoint * k_ScaledPixelsPerPointMultiplier;
if (Mathf.Abs(m_Velocity.x) <= scaledSpeedLimit ||
touchScrollBehavior == TouchScrollBehavior.Elastic && (scrollOffset.x < m_LowBounds.x || scrollOffset.x > m_HighBounds.x))
{
m_Velocity.x = 0;
}
if (Mathf.Abs(m_Velocity.y) <= scaledSpeedLimit ||
touchScrollBehavior == TouchScrollBehavior.Elastic && (scrollOffset.y < m_LowBounds.y || scrollOffset.y > m_HighBounds.y))
{
m_Velocity.y = 0;
}
scrollOffset += additionalOffset;
}
else
{
m_Velocity = Vector2.zero;
}
}
private void PostPointerUpAnimation()
{
elapsedTimeSinceLastVerticalTouchScroll = Time.unscaledTime - previousVerticalTouchScrollTimeStamp;
previousVerticalTouchScrollTimeStamp = Time.unscaledTime;
elapsedTimeSinceLastHorizontalTouchScroll = Time.unscaledTime - previousHorizontalTouchScrollTimeStamp;
previousHorizontalTouchScrollTimeStamp = Time.unscaledTime;
ApplyScrollInertia();
SpringBack();
// This compares with epsilon.
if (m_SpringBackVelocity == Vector2.zero && m_Velocity == Vector2.zero)
{
m_PostPointerUpAnimation.Pause();
elapsedTimeSinceLastVerticalTouchScroll = 0f;
elapsedTimeSinceLastHorizontalTouchScroll = 0f;
previousVerticalTouchScrollTimeStamp = 0f;
previousHorizontalTouchScrollTimeStamp = 0f;
}
}
void OnPointerDown(PointerDownEvent evt)
{
if (evt.pointerType == PointerType.mouse || !evt.isPrimary)
return;
if (evt.pointerId != PointerId.invalidPointerId)
{
ReleaseScrolling(evt.pointerId, evt.target);
}
m_PostPointerUpAnimation?.Pause();
var touchStopsVelocityOnly = Mathf.Abs(m_Velocity.x) > 10 || Mathf.Abs(m_Velocity.y) > 10;
m_TouchPointerMoveAllowed = true;
m_StartedMoving = false;
InitTouchScrolling(evt.position);
if (touchStopsVelocityOnly)
{
contentContainer.CapturePointer(evt.pointerId);
contentContainer.panel.PreventCompatibilityMouseEvents(evt.pointerId);
evt.StopPropagation();
m_TouchStoppedVelocity = true;
}
}
void OnPointerMove(PointerMoveEvent evt)
{
if (evt.pointerType == PointerType.mouse || !evt.isPrimary || !m_TouchPointerMoveAllowed)
return;
if (evt.isHandledByDraggable)
{
m_PointerStartPosition = evt.position;
m_StartPosition = scrollOffset;
return;
}
Vector2 position = evt.position;
var delta = position - m_PointerStartPosition;
if (mode == ScrollViewMode.Horizontal)
delta.y = 0;
else if (mode == ScrollViewMode.Vertical)
delta.x = 0;
if (!m_TouchStoppedVelocity && !m_StartedMoving && delta.sqrMagnitude < ScrollThresholdSquared)
return;
var scrollResult = ComputeTouchScrolling(evt.position);
if (scrollResult != TouchScrollingResult.Forward)
{
evt.isHandledByDraggable = true;
evt.StopPropagation();
if (!contentContainer.HasPointerCapture(evt.pointerId))
contentContainer.CapturePointer(evt.pointerId);
}
else
{
m_Velocity = Vector2.zero;
}
}
void OnPointerCancel(PointerCancelEvent evt)
{
ReleaseScrolling(evt.pointerId, evt.target);
}
void OnPointerUp(PointerUpEvent evt)
{
if (ReleaseScrolling(evt.pointerId, evt.target))
{
contentContainer.panel.PreventCompatibilityMouseEvents(evt.pointerId);
evt.StopPropagation();
}
}
// Internal for tests.
internal enum TouchScrollingResult
{
Apply,
Forward,
Block
}
// Internal for tests.
internal void InitTouchScrolling(Vector2 position)
{
m_PointerStartPosition = position;
m_StartPosition = scrollOffset;
m_Velocity = Vector2.zero;
m_SpringBackVelocity = Vector2.zero;
m_LowBounds = new Vector2(
Mathf.Min(horizontalScroller.lowValue, horizontalScroller.highValue),
Mathf.Min(verticalScroller.lowValue, verticalScroller.highValue));
m_HighBounds = new Vector2(
Mathf.Max(horizontalScroller.lowValue, horizontalScroller.highValue),
Mathf.Max(verticalScroller.lowValue, verticalScroller.highValue));
}
// Internal for tests.
internal TouchScrollingResult ComputeTouchScrolling(Vector2 position)
{
// Calculate offset based on touch scroll behavior.
Vector2 newScrollOffset;
if (touchScrollBehavior == TouchScrollBehavior.Clamped)
{
newScrollOffset = m_StartPosition - (position - m_PointerStartPosition);
newScrollOffset = Vector2.Max(newScrollOffset, m_LowBounds);
newScrollOffset = Vector2.Min(newScrollOffset, m_HighBounds);
}
else if (touchScrollBehavior == TouchScrollBehavior.Elastic)
{
Vector2 deltaPointer = position - m_PointerStartPosition;
newScrollOffset.x = ComputeElasticOffset(deltaPointer.x, m_StartPosition.x,
m_LowBounds.x, m_LowBounds.x - contentViewport.resolvedStyle.width,
m_HighBounds.x, m_HighBounds.x + contentViewport.resolvedStyle.width);
newScrollOffset.y = ComputeElasticOffset(deltaPointer.y, m_StartPosition.y,
m_LowBounds.y, m_LowBounds.y - contentViewport.resolvedStyle.height,
m_HighBounds.y, m_HighBounds.y + contentViewport.resolvedStyle.height);
previousVerticalTouchScrollTimeStamp = Time.unscaledTime;
previousHorizontalTouchScrollTimeStamp = Time.unscaledTime;
}
else
{
newScrollOffset = m_StartPosition - (position - m_PointerStartPosition);
}
// Cancel opposite axis if mode is set to only a single direction.
if (mode == ScrollViewMode.Vertical)
newScrollOffset.x = m_LowBounds.x;
else if (mode == ScrollViewMode.Horizontal)
newScrollOffset.y = m_LowBounds.y;
var shouldScrollOffsetChange = scrollOffset != newScrollOffset;
if (shouldScrollOffsetChange)
{
return ApplyTouchScrolling(newScrollOffset) ? TouchScrollingResult.Apply : TouchScrollingResult.Forward;
}
var shouldBlock = m_StartedMoving && nestedInteractionKind != NestedInteractionKind.ForwardScrolling;
return shouldBlock ? TouchScrollingResult.Block : TouchScrollingResult.Forward;
}
bool ApplyTouchScrolling(Vector2 newScrollOffset)
{
m_StartedMoving = true;
if (hasInertia)
{
// Reset velocity if we reached bounds.
if (newScrollOffset == m_LowBounds || newScrollOffset == m_HighBounds)
{
m_Velocity = Vector2.zero;
scrollOffset = newScrollOffset;
return false;
}
// Account for idle pointer time.
if (m_LastVelocityLerpTime > 0)
{
var deltaTimeSinceLastLerp = Time.unscaledTime - m_LastVelocityLerpTime;
m_Velocity = Vector2.Lerp(m_Velocity, Vector2.zero, deltaTimeSinceLastLerp * k_VelocityLerpTimeFactor);
}
m_LastVelocityLerpTime = Time.unscaledTime;
var deltaTime = k_TouchScrollInertiaBaseTimeInterval;
var newVelocity = (newScrollOffset - scrollOffset) / deltaTime;
m_Velocity = Vector2.Lerp(m_Velocity, newVelocity, deltaTime * k_VelocityLerpTimeFactor);
}
var scrollOffsetChanged = scrollOffset != newScrollOffset;
scrollOffset = newScrollOffset;
return scrollOffsetChanged;
}
bool ReleaseScrolling(int pointerId, IEventHandler target)
{
m_TouchStoppedVelocity = false;
m_StartedMoving = false;
m_TouchPointerMoveAllowed = false;
if (target != contentContainer || !contentContainer.HasPointerCapture(pointerId))
return false;
previousVerticalTouchScrollTimeStamp = Time.unscaledTime;
previousHorizontalTouchScrollTimeStamp = Time.unscaledTime;
if (touchScrollBehavior == TouchScrollBehavior.Elastic || hasInertia)
{
ExecuteElasticSpringAnimation();
}
contentContainer.ReleasePointer(pointerId);
return true;
}
void ExecuteElasticSpringAnimation()
{
ComputeInitialSpringBackVelocity();
if (m_PostPointerUpAnimation == null)
{
m_PostPointerUpAnimation = schedule.Execute(PostPointerUpAnimation).Every(m_ElasticAnimationIntervalMs);
}
else
{
m_PostPointerUpAnimation.Resume();
}
}
void AdjustScrollers()
{
float horizontalFactor = contentContainer.boundingBox.width > UIRUtility.k_Epsilon ? contentViewport.layout.width / contentContainer.boundingBox.width : 1f;
float verticalFactor = contentContainer.boundingBox.height > UIRUtility.k_Epsilon ? contentViewport.layout.height / contentContainer.boundingBox.height : 1f;
horizontalScroller.Adjust(horizontalFactor);
verticalScroller.Adjust(verticalFactor);
}
internal void UpdateScrollers(bool displayHorizontal, bool displayVertical)
{
AdjustScrollers();
// Set availability
horizontalScroller.SetEnabled(scrollableWidth > 0);
verticalScroller.SetEnabled(scrollableHeight > 0);
var newShowHorizontal = displayHorizontal && m_HorizontalScrollerVisibility != ScrollerVisibility.Hidden;
var newShowVertical = displayVertical && m_VerticalScrollerVisibility != ScrollerVisibility.Hidden;
var newHorizontalDisplay = newShowHorizontal ? DisplayStyle.Flex : DisplayStyle.None;
var newVerticalDisplay = newShowVertical ? DisplayStyle.Flex : DisplayStyle.None;
// Set display as necessary
if (newHorizontalDisplay != horizontalScroller.style.display)
{
horizontalScroller.style.display = newHorizontalDisplay;
}
if (newVerticalDisplay != verticalScroller.style.display)
{
verticalScroller.style.display = newVerticalDisplay;
}
// Need to set always, for touch scrolling.
verticalScroller.lowValue = 0f;
verticalScroller.highValue = float.IsNaN(scrollableHeight) ? verticalScroller.highValue : scrollableHeight;
horizontalScroller.lowValue = 0f;
horizontalScroller.highValue = float.IsNaN(scrollableWidth) ? horizontalScroller.highValue : scrollableWidth;
}
private void OnScrollersGeometryChanged(GeometryChangedEvent evt)
{
if (evt.oldRect.size == evt.newRect.size)
{
return;
}
var newShowHorizontal = needsHorizontal && m_HorizontalScrollerVisibility != ScrollerVisibility.Hidden;
// Align the right side of the horizontal scroller with the left side of the vertical scroller.
if (newShowHorizontal)
{
horizontalScroller.style.marginRight = verticalScroller.layout.width;
}
AdjustScrollers();
}
// TODO: Same behaviour as IMGUI Scroll view
void OnScrollWheel(WheelEvent evt)
{
var updateContentViewTransform = false;
var canUseVerticalScroll = mode != ScrollViewMode.Horizontal && scrollableHeight > 0;
var canUseHorizontalScroll = mode != ScrollViewMode.Vertical && scrollableWidth > 0;
var horizontalScrollDelta = canUseHorizontalScroll && !canUseVerticalScroll ? evt.delta.y : evt.delta.x;
if ((canUseHorizontalScroll || canUseVerticalScroll) && !m_MouseWheelScrollSizeIsInline && m_SingleLineHeightDirtyFlag)
{
ReadSingleLineHeight();
}
var mouseScrollFactor = m_MouseWheelScrollSizeIsInline ? mouseWheelScrollSize : m_SingleLineHeight;
if (canUseVerticalScroll)
{
var oldVerticalValue = verticalScroller.value;
verticalScroller.value += evt.delta.y * (verticalScroller.lowValue < verticalScroller.highValue ? 1f : -1f) * mouseScrollFactor;
if (nestedInteractionKind == NestedInteractionKind.StopScrolling || !Mathf.Approximately(verticalScroller.value, oldVerticalValue))
{
evt.StopPropagation();
updateContentViewTransform = true;
}
}
if (canUseHorizontalScroll)
{
var oldHorizontalValue = horizontalScroller.value;
horizontalScroller.value += horizontalScrollDelta * (horizontalScroller.lowValue < horizontalScroller.highValue ? 1f : -1f) * mouseScrollFactor;
if (nestedInteractionKind == NestedInteractionKind.StopScrolling || !Mathf.Approximately(horizontalScroller.value, oldHorizontalValue))
{
evt.StopPropagation();
updateContentViewTransform = true;
}
}
if (updateContentViewTransform)
{
UpdateElasticBehaviour();
UpdateContentViewTransform();
}
}
void OnRootCustomStyleResolved(CustomStyleResolvedEvent evt)
{
// Do not read single line height yet: SV or one of its ancestors might have custom variable values that affect it.
MarkSingleLineHeightDirty();
}
void MarkSingleLineHeightDirty()
{
m_SingleLineHeightDirtyFlag = true;
}
void OnRootPointerUp(PointerUpEvent evt)
{
m_TouchPointerMoveAllowed = false;
}
void ReadSingleLineHeight()
{
var currentParent = (VisualElement) this;
while (currentParent != null)
{
if (currentParent.computedStyle.customProperties != null &&
currentParent.computedStyle.customProperties.TryGetValue(k_SingleLineHeightPropertyName, out var customProp))
{
m_SingleLineHeightDirtyFlag = false;
if (customProp.sheet.TryReadDimension(customProp.handle, out var dimension))
{
m_SingleLineHeight = dimension.value;
return;
}
}
else if (currentParent == m_AttachedRootVisualContainer)
{
break;
}
currentParent = currentParent.parent;
}
m_SingleLineHeight = UIElementsUtility.singleLineHeight;
m_SingleLineHeightDirtyFlag = false;
}
void UpdateElasticBehaviour()
{
if (touchScrollBehavior == TouchScrollBehavior.Elastic)
{
m_LowBounds = new Vector2(
Mathf.Min(horizontalScroller.lowValue, horizontalScroller.highValue),
Mathf.Min(verticalScroller.lowValue, verticalScroller.highValue));
m_HighBounds = new Vector2(
Mathf.Max(horizontalScroller.lowValue, horizontalScroller.highValue),
Mathf.Max(verticalScroller.lowValue, verticalScroller.highValue));
ExecuteElasticSpringAnimation();
}
}
internal void SetScrollOffsetWithoutNotify(Vector2 value)
{
horizontalScroller.slider.SetValueWithoutNotify(value.x);
verticalScroller.slider.SetValueWithoutNotify(value.y);
// Can't directly use the value's x and y as they might be clamped by the slider.
m_ScrollOffset = new Vector2(horizontalScroller.value, verticalScroller.value);
SaveViewData();
}
internal override void OnViewDataReady()
{
base.OnViewDataReady();
// There is a viewDataKey set in the inspector's scrollview. For PropertyEditor, we disable the scrollbar's
// persistance by removing the viewDataKey. To comply with this use case, we want to skip the restore as not
// every inspector will be used as a PropertyEditor.
if (string.IsNullOrEmpty(verticalScroller.viewDataKey) &&
string.IsNullOrEmpty(verticalScroller.slider.viewDataKey) &&
string.IsNullOrEmpty(horizontalScroller.viewDataKey) &&
string.IsNullOrEmpty(horizontalScroller.slider.viewDataKey))
{
return;
}
var key = GetFullHierarchicalViewDataKey();
OverwriteFromViewData(this, key);
UpdateContentViewTransform();
}
}
}