using System;
using System.Collections;
using UnityEngine.Events;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
[AddComponentMenu("UI/Scrollbar", 36)]
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
///
/// A standard scrollbar with a variable sized handle that can be dragged between 0 and 1.
///
///
/// The slider component is a Selectable that controls a handle which follow the current value and is sized according to the size property.
/// The anchors of the handle RectTransforms are driven by the Scrollbar. The handle can be a direct child of the GameObject with the Scrollbar, or intermediary RectTransforms can be placed in between for additional control.
/// When a change to the scrollbar value occurs, a callback is sent to any registered listeners of onValueChanged.
///
public class Scrollbar : Selectable, IBeginDragHandler, IDragHandler, IInitializePotentialDragHandler, ICanvasElement
{
///
/// Setting that indicates one of four directions the scrollbar will travel.
///
public enum Direction
{
///
/// Starting position is the Left.
///
LeftToRight,
///
/// Starting position is the Right
///
RightToLeft,
///
/// Starting position is the Bottom.
///
BottomToTop,
///
/// Starting position is the Top.
///
TopToBottom,
}
[Serializable]
///
/// UnityEvent callback for when a scrollbar is scrolled.
///
public class ScrollEvent : UnityEvent {}
[SerializeField]
private RectTransform m_HandleRect;
///
/// The RectTransform to use for the handle.
///
public RectTransform handleRect { get { return m_HandleRect; } set { if (SetPropertyUtility.SetClass(ref m_HandleRect, value)) { UpdateCachedReferences(); UpdateVisuals(); } } }
// Direction of movement.
[SerializeField]
private Direction m_Direction = Direction.LeftToRight;
///
/// The direction of the scrollbar from minimum to maximum value.
///
public Direction direction { get { return m_Direction; } set { if (SetPropertyUtility.SetStruct(ref m_Direction, value)) UpdateVisuals(); } }
protected Scrollbar()
{}
[Range(0f, 1f)]
[SerializeField]
private float m_Value;
///
/// The current value of the scrollbar, between 0 and 1.
///
public float value
{
get
{
float val = m_Value;
if (m_NumberOfSteps > 1)
val = Mathf.Round(val * (m_NumberOfSteps - 1)) / (m_NumberOfSteps - 1);
return val;
}
set
{
Set(value);
}
}
///
/// Set the value of the scrollbar without invoking onValueChanged callback.
///
/// The new value for the scrollbar.
public virtual void SetValueWithoutNotify(float input)
{
Set(input, false);
}
[Range(0f, 1f)]
[SerializeField]
private float m_Size = 0.2f;
///
/// The size of the scrollbar handle where 1 means it fills the entire scrollbar.
///
public float size { get { return m_Size; } set { if (SetPropertyUtility.SetStruct(ref m_Size, Mathf.Clamp01(value))) UpdateVisuals(); } }
[Range(0, 11)]
[SerializeField]
private int m_NumberOfSteps = 0;
///
/// The number of steps to use for the value. A value of 0 disables use of steps.
///
public int numberOfSteps { get { return m_NumberOfSteps; } set { if (SetPropertyUtility.SetStruct(ref m_NumberOfSteps, value)) { Set(m_Value); UpdateVisuals(); } } }
[Space(6)]
[SerializeField]
private ScrollEvent m_OnValueChanged = new ScrollEvent();
///
/// Handling for when the scrollbar value is changed.
///
///
/// Allow for delegate-based subscriptions for faster events than 'eventReceiver', and allowing for multiple receivers.
///
public ScrollEvent onValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }
// Private fields
private RectTransform m_ContainerRect;
// The offset from handle position to mouse down position
private Vector2 m_Offset = Vector2.zero;
// Size of each step.
float stepSize { get { return (m_NumberOfSteps > 1) ? 1f / (m_NumberOfSteps - 1) : 0.1f; } }
// field is never assigned warning
#pragma warning disable 649
private DrivenRectTransformTracker m_Tracker;
#pragma warning restore 649
private Coroutine m_PointerDownRepeat;
private bool isPointerDownAndNotDragging = false;
// This "delayed" mechanism is required for case 1037681.
private bool m_DelayedUpdateVisuals = false;
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
m_Size = Mathf.Clamp01(m_Size);
//This can be invoked before OnEnabled is called. So we shouldn't be accessing other objects, before OnEnable is called.
if (IsActive())
{
UpdateCachedReferences();
Set(m_Value, false);
// Update rects (in next update) since other things might affect them even if value didn't change.
m_DelayedUpdateVisuals = true;
}
if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
}
#endif // if UNITY_EDITOR
public virtual void Rebuild(CanvasUpdate executing)
{
#if UNITY_EDITOR
if (executing == CanvasUpdate.Prelayout)
onValueChanged.Invoke(value);
#endif
}
///
/// See ICanvasElement.LayoutComplete.
///
public virtual void LayoutComplete()
{}
///
/// See ICanvasElement.GraphicUpdateComplete.
///
public virtual void GraphicUpdateComplete()
{}
protected override void OnEnable()
{
base.OnEnable();
UpdateCachedReferences();
Set(m_Value, false);
// Update rects since they need to be initialized correctly.
UpdateVisuals();
}
protected override void OnDisable()
{
m_Tracker.Clear();
base.OnDisable();
}
///
/// Update the rect based on the delayed update visuals.
/// Got around issue of calling sendMessage from onValidate.
///
protected virtual void Update()
{
if (m_DelayedUpdateVisuals)
{
m_DelayedUpdateVisuals = false;
UpdateVisuals();
}
}
void UpdateCachedReferences()
{
if (m_HandleRect && m_HandleRect.parent != null)
m_ContainerRect = m_HandleRect.parent.GetComponent();
else
m_ContainerRect = null;
}
void Set(float input, bool sendCallback = true)
{
float currentValue = m_Value;
// bugfix (case 802330) clamp01 input in callee before calling this function, this allows inertia from dragging content to go past extremities without being clamped
m_Value = input;
// If the stepped value doesn't match the last one, it's time to update
if (currentValue == value)
return;
UpdateVisuals();
if (sendCallback)
{
UISystemProfilerApi.AddMarker("Scrollbar.value", this);
m_OnValueChanged.Invoke(value);
}
}
protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
//This can be invoked before OnEnabled is called. So we shouldn't be accessing other objects, before OnEnable is called.
if (!IsActive())
return;
UpdateVisuals();
}
enum Axis
{
Horizontal = 0,
Vertical = 1
}
Axis axis { get { return (m_Direction == Direction.LeftToRight || m_Direction == Direction.RightToLeft) ? Axis.Horizontal : Axis.Vertical; } }
bool reverseValue { get { return m_Direction == Direction.RightToLeft || m_Direction == Direction.TopToBottom; } }
// Force-update the scroll bar. Useful if you've changed the properties and want it to update visually.
private void UpdateVisuals()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
UpdateCachedReferences();
#endif
m_Tracker.Clear();
if (m_ContainerRect != null)
{
m_Tracker.Add(this, m_HandleRect, DrivenTransformProperties.Anchors);
Vector2 anchorMin = Vector2.zero;
Vector2 anchorMax = Vector2.one;
float movement = Mathf.Clamp01(value) * (1 - size);
if (reverseValue)
{
anchorMin[(int)axis] = 1 - movement - size;
anchorMax[(int)axis] = 1 - movement;
}
else
{
anchorMin[(int)axis] = movement;
anchorMax[(int)axis] = movement + size;
}
m_HandleRect.anchorMin = anchorMin;
m_HandleRect.anchorMax = anchorMax;
}
}
// Update the scroll bar's position based on the mouse.
void UpdateDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (m_ContainerRect == null)
return;
Vector2 position = Vector2.zero;
if (!MultipleDisplayUtilities.GetRelativeMousePositionForDrag(eventData, ref position))
return;
Vector2 localCursor;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(m_ContainerRect, position, eventData.pressEventCamera, out localCursor))
return;
Vector2 handleCenterRelativeToContainerCorner = localCursor - m_Offset - m_ContainerRect.rect.position;
Vector2 handleCorner = handleCenterRelativeToContainerCorner - (m_HandleRect.rect.size - m_HandleRect.sizeDelta) * 0.5f;
float parentSize = axis == 0 ? m_ContainerRect.rect.width : m_ContainerRect.rect.height;
float remainingSize = parentSize * (1 - size);
if (remainingSize <= 0)
return;
DoUpdateDrag(handleCorner, remainingSize);
}
//this function is testable, it is found using reflection in ScrollbarClamp test
private void DoUpdateDrag(Vector2 handleCorner, float remainingSize)
{
switch (m_Direction)
{
case Direction.LeftToRight:
Set(Mathf.Clamp01(handleCorner.x / remainingSize));
break;
case Direction.RightToLeft:
Set(Mathf.Clamp01(1f - (handleCorner.x / remainingSize)));
break;
case Direction.BottomToTop:
Set(Mathf.Clamp01(handleCorner.y / remainingSize));
break;
case Direction.TopToBottom:
Set(Mathf.Clamp01(1f - (handleCorner.y / remainingSize)));
break;
}
}
private bool MayDrag(PointerEventData eventData)
{
return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left;
}
///
/// Handling for when the scrollbar value is begin being dragged.
///
public virtual void OnBeginDrag(PointerEventData eventData)
{
isPointerDownAndNotDragging = false;
if (!MayDrag(eventData))
return;
if (m_ContainerRect == null)
return;
m_Offset = Vector2.zero;
if (RectTransformUtility.RectangleContainsScreenPoint(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.enterEventCamera))
{
Vector2 localMousePos;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.pressEventCamera, out localMousePos))
m_Offset = localMousePos - m_HandleRect.rect.center;
}
}
///
/// Handling for when the scrollbar value is dragged.
///
public virtual void OnDrag(PointerEventData eventData)
{
if (!MayDrag(eventData))
return;
if (m_ContainerRect != null)
UpdateDrag(eventData);
}
///
/// Event triggered when pointer is pressed down on the scrollbar.
///
public override void OnPointerDown(PointerEventData eventData)
{
if (!MayDrag(eventData))
return;
base.OnPointerDown(eventData);
isPointerDownAndNotDragging = true;
m_PointerDownRepeat = StartCoroutine(ClickRepeat(eventData.pointerPressRaycast.screenPosition, eventData.enterEventCamera));
}
protected IEnumerator ClickRepeat(PointerEventData eventData)
{
return ClickRepeat(eventData.pointerPressRaycast.screenPosition, eventData.enterEventCamera);
}
///
/// Coroutine function for handling continual press during Scrollbar.OnPointerDown.
///
protected IEnumerator ClickRepeat(Vector2 screenPosition, Camera camera)
{
while (isPointerDownAndNotDragging)
{
if (!RectTransformUtility.RectangleContainsScreenPoint(m_HandleRect, screenPosition, camera))
{
Vector2 localMousePos;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_HandleRect, screenPosition, camera, out localMousePos))
{
var axisCoordinate = axis == 0 ? localMousePos.x : localMousePos.y;
// modifying value depending on direction, fixes (case 925824)
float change = axisCoordinate < 0 ? size : -size;
value += reverseValue ? change : -change;
value = Mathf.Clamp01(value);
// Only keep 4 decimals of precision
value = Mathf.Round(value * 10000f) / 10000f;
}
}
yield return new WaitForEndOfFrame();
}
StopCoroutine(m_PointerDownRepeat);
}
///
/// Event triggered when pointer is released after pressing on the scrollbar.
///
public override void OnPointerUp(PointerEventData eventData)
{
base.OnPointerUp(eventData);
isPointerDownAndNotDragging = false;
}
///
/// Handling for movement events.
///
public override void OnMove(AxisEventData eventData)
{
if (!IsActive() || !IsInteractable())
{
base.OnMove(eventData);
return;
}
switch (eventData.moveDir)
{
case MoveDirection.Left:
if (axis == Axis.Horizontal && FindSelectableOnLeft() == null)
Set(Mathf.Clamp01(reverseValue ? value + stepSize : value - stepSize));
else
base.OnMove(eventData);
break;
case MoveDirection.Right:
if (axis == Axis.Horizontal && FindSelectableOnRight() == null)
Set(Mathf.Clamp01(reverseValue ? value - stepSize : value + stepSize));
else
base.OnMove(eventData);
break;
case MoveDirection.Up:
if (axis == Axis.Vertical && FindSelectableOnUp() == null)
Set(Mathf.Clamp01(reverseValue ? value - stepSize : value + stepSize));
else
base.OnMove(eventData);
break;
case MoveDirection.Down:
if (axis == Axis.Vertical && FindSelectableOnDown() == null)
Set(Mathf.Clamp01(reverseValue ? value + stepSize : value - stepSize));
else
base.OnMove(eventData);
break;
}
}
///
/// Prevents selection if we we move on the Horizontal axis. See Selectable.FindSelectableOnLeft.
///
public override Selectable FindSelectableOnLeft()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Horizontal)
return null;
return base.FindSelectableOnLeft();
}
///
/// Prevents selection if we we move on the Horizontal axis. See Selectable.FindSelectableOnRight.
///
public override Selectable FindSelectableOnRight()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Horizontal)
return null;
return base.FindSelectableOnRight();
}
///
/// Prevents selection if we we move on the Vertical axis. See Selectable.FindSelectableOnUp.
///
public override Selectable FindSelectableOnUp()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Vertical)
return null;
return base.FindSelectableOnUp();
}
///
/// Prevents selection if we we move on the Vertical axis. See Selectable.FindSelectableOnDown.
///
public override Selectable FindSelectableOnDown()
{
if (navigation.mode == Navigation.Mode.Automatic && axis == Axis.Vertical)
return null;
return base.FindSelectableOnDown();
}
///
/// See: IInitializePotentialDragHandler.OnInitializePotentialDrag
///
public virtual void OnInitializePotentialDrag(PointerEventData eventData)
{
eventData.useDragThreshold = false;
}
///
/// Set the direction of the scrollbar, optionally setting the layout as well.
///
/// The direction of the scrollbar.
/// Should the layout be flipped together with the direction?
public void SetDirection(Direction direction, bool includeRectLayouts)
{
Axis oldAxis = axis;
bool oldReverse = reverseValue;
this.direction = direction;
if (!includeRectLayouts)
return;
if (axis != oldAxis)
RectTransformUtility.FlipLayoutAxes(transform as RectTransform, true, true);
if (reverseValue != oldReverse)
RectTransformUtility.FlipLayoutOnAxis(transform as RectTransform, (int)axis, true, true);
}
}
}