Skip to content

Instantly share code, notes, and snippets.

@stramit
Created September 22, 2014 09:55
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stramit/b5f41bf07a3947ac4a73 to your computer and use it in GitHub Desktop.
Save stramit/b5f41bf07a3947ac4a73 to your computer and use it in GitHub Desktop.
Scroll Rect
using System;
using UnityEngine.Events;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
[AddComponentMenu ("UI/Scroll Rect", 33)]
[SelectionBase]
[ExecuteInEditMode]
[RequireComponent(typeof(RectTransform))]
public class ScrollRect : UIBehaviour, IPopulateDragThresholdHandler, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler, ICanvasElement
{
public enum MovementType
{
Unrestricted, // Unrestricted movement -- can scroll forever
Elastic, // Restricted but flexible -- can go past the edges, but springs back in place
Clamped, // Restricted movement where it's not possible to go past the edges
}
[Serializable]
public class ScrollRectEvent : UnityEvent<Vector2> { }
[SerializeField]
private RectTransform m_Content;
public RectTransform content { get { return m_Content; } set { m_Content = value; } }
[SerializeField]
private bool m_Horizontal = true;
public bool horizontal { get { return m_Horizontal; } set { m_Horizontal = value; } }
[SerializeField]
private bool m_Vertical = true;
public bool vertical { get { return m_Vertical; } set { m_Vertical = value; } }
[SerializeField]
private MovementType m_MovementType = MovementType.Elastic;
public MovementType movementType { get { return m_MovementType; } set { m_MovementType = value; } }
[SerializeField]
private float m_Elasticity = 0.1f; // Only used for MovementType.Elastic
public float elasticity { get { return m_Elasticity; } set { m_Elasticity = value; } }
[SerializeField]
private bool m_Inertia = true;
public bool inertia { get { return m_Inertia; } set { m_Inertia = value; } }
[SerializeField]
private float m_DecelerationRate = 0.135f; // Only used when inertia is enabled
public float decelerationRate { get { return m_DecelerationRate; } set { m_DecelerationRate = value; } }
[SerializeField]
private float m_ScrollSensitivity = 1.0f;
public float scrollSensitivity { get { return m_ScrollSensitivity; } set { m_ScrollSensitivity = value; } }
[SerializeField]
private int m_DragThreshold = 15;
public int dragThreshold { get { return m_DragThreshold; } set { m_DragThreshold = value; } }
[SerializeField]
private Scrollbar m_HorizontalScrollbar;
public Scrollbar horizontalScrollbar
{
get
{
return m_HorizontalScrollbar;
}
set
{
if (m_HorizontalScrollbar)
m_HorizontalScrollbar.onValueChanged.RemoveListener (SetHorizontalNormalizedPosition);
m_HorizontalScrollbar = value;
if (m_HorizontalScrollbar)
m_HorizontalScrollbar.onValueChanged.AddListener (SetHorizontalNormalizedPosition);
}
}
[SerializeField]
private Scrollbar m_VerticalScrollbar;
public Scrollbar verticalScrollbar
{
get
{
return m_VerticalScrollbar;
}
set
{
if (m_VerticalScrollbar)
m_VerticalScrollbar.onValueChanged.RemoveListener (SetVerticalNormalizedPosition);
m_VerticalScrollbar = value;
if (m_VerticalScrollbar)
m_VerticalScrollbar.onValueChanged.AddListener (SetVerticalNormalizedPosition);
}
}
[SerializeField]
private ScrollRectEvent m_OnValueChanged = new ScrollRectEvent ();
public ScrollRectEvent onValueChanged { get { return m_OnValueChanged; } set { m_OnValueChanged = value; } }
// The offset from handle position to mouse down position
private Vector2 m_PointerStartLocalCursor = Vector2.zero;
private Vector2 m_ContentStartPosition = Vector2.zero;
private RectTransform m_ViewRect;
private Bounds m_ContentBounds;
private Bounds m_ViewBounds;
private Vector2 m_Velocity;
public Vector2 velocity { get { return m_Velocity; } set { m_Velocity = value; } }
private bool m_Dragging = false;
private Vector2 m_PrevPosition = Vector2.zero;
private Bounds m_PrevContentBounds = new Bounds ();
private Bounds m_PrevViewBounds = new Bounds ();
protected ScrollRect()
{}
public virtual void Rebuild (CanvasUpdate executing)
{
if (executing != CanvasUpdate.PostLayout)
return;
UpdateBounds ();
UpdateScrollbars (Vector2.zero);
UpdatePrevData ();
}
protected override void OnEnable ()
{
base.OnEnable ();
m_ViewRect = transform as RectTransform;
if (m_HorizontalScrollbar)
m_HorizontalScrollbar.onValueChanged.AddListener (SetHorizontalNormalizedPosition);
if (m_VerticalScrollbar)
m_VerticalScrollbar.onValueChanged.AddListener (SetVerticalNormalizedPosition);
// Note: We need to ensure that the layout calculations have run before UpdateBounds is called.
// CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild would work for this
// if we only called UpdateBounds outselves from inside this class.
// Unfortunate it's also being invoked by the OnEnable and OnValidate callbacks of Scrollbar,
// so it may well be called before the layout code has had a chance to run naturally.
// Calling Canvas.ForceUpdateCanvases seems to fix the issue though theoretically it might not always work,
// if certain layout elements hadn't set themselves as dirty before the ForceUpdateCanvases method is run.
// Ideally we should find a more robust method, but it seems tricky without removing validation logic in
// OnEnable and OnValidate of other components.
Canvas.ForceUpdateCanvases ();
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild (this);
}
protected override void OnDisable ()
{
base.OnDisable ();
if (m_HorizontalScrollbar)
m_HorizontalScrollbar.onValueChanged.RemoveListener (SetHorizontalNormalizedPosition);
if (m_VerticalScrollbar)
m_VerticalScrollbar.onValueChanged.RemoveListener (SetVerticalNormalizedPosition);
}
public override bool IsActive ()
{
return base.IsActive () && m_Content != null;
}
public void StopMovement()
{
m_Velocity = Vector2.zero;
}
public void OnScroll (PointerEventData data)
{
if (!IsActive ())
return;
Vector2 delta = data.scrollDelta;
// Down is positive for scroll events, while in UI system up is positive.
delta.y *= -1;
if (vertical && !horizontal)
{
if (Mathf.Abs (delta.x) > Mathf.Abs (delta.y))
delta.y = delta.x;
delta.x = 0;
}
if (horizontal && !vertical)
{
if (Mathf.Abs (delta.y) > Mathf.Abs (delta.x))
delta.x = delta.y;
delta.y = 0;
}
Vector2 position = m_Content.anchoredPosition;
position += delta * m_ScrollSensitivity;
m_Content.anchoredPosition = position;
UpdateBounds ();
}
public void OnPopulateDragThreshold(PointerEventData eventData)
{
eventData.dragThreshold = dragThreshold;
m_Velocity = Vector2.zero;
}
public void OnBeginDrag (PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (!IsActive ())
return;
m_PointerStartLocalCursor = Vector2.zero;
RectTransformUtility.ScreenPointToLocalPointInRectangle (m_ViewRect, eventData.position, eventData.pressEventCamera, out m_PointerStartLocalCursor);
m_ContentStartPosition = m_Content.anchoredPosition;
m_Dragging = true;
UpdateBounds ();
}
public void OnEndDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
m_Dragging = false;
}
public void OnDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
if (!IsActive())
return;
Vector2 localCursor;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle (m_ViewRect, eventData.position, eventData.pressEventCamera, out localCursor))
return;
UpdateBounds ();
var pointerDelta = localCursor - m_PointerStartLocalCursor;
Vector2 position = m_ContentStartPosition + pointerDelta;
// Offset to get content into place in the view.
Vector2 offset = CalculateOffset (position - m_Content.anchoredPosition);
position += offset;
if (m_MovementType == MovementType.Elastic)
{
if (offset.x != 0)
position.x = position.x - RubberDelta (offset.x, m_ViewBounds.size.x);
if (offset.y != 0)
position.y = position.y - RubberDelta (offset.y, m_ViewBounds.size.y);
}
if (!m_Horizontal)
position.x = m_Content.anchoredPosition.x;
if (!m_Vertical)
position.y = m_Content.anchoredPosition.y;
m_Content.anchoredPosition = position;
}
protected virtual void LateUpdate ()
{
if (!m_Content)
return;
float deltaTime = Time.unscaledDeltaTime;
Vector2 offset = CalculateOffset (Vector2.zero);
if (!m_Dragging && (offset != Vector2.zero || m_Velocity != Vector2.zero))
{
Vector2 position = m_Content.anchoredPosition;
for (int axis = 0; axis < 2; axis++)
{
// Apply spring physics if movement is elastic and content has an offset from the view.
if (m_MovementType == MovementType.Elastic && offset[axis] != 0)
{
float speed = m_Velocity[axis];
position[axis] = Mathf.SmoothDamp (m_Content.anchoredPosition[axis], m_Content.anchoredPosition[axis] + offset[axis], ref speed, m_Elasticity, Mathf.Infinity, deltaTime);
m_Velocity[axis] = speed;
}
// Else move content according to velocity with deceleration applied.
else if (m_Inertia)
{
m_Velocity[axis] *= Mathf.Pow (m_DecelerationRate, deltaTime);
if (Mathf.Abs (m_Velocity[axis]) < 1)
m_Velocity[axis] = 0;
position[axis] += m_Velocity[axis] * deltaTime;
}
// If we have neither elaticity or friction, there shouldn't be any velocity.
else
{
m_Velocity[axis] = 0;
}
}
if (m_Velocity != Vector2.zero)
{
if (m_MovementType == MovementType.Clamped)
{
offset = CalculateOffset (position - m_Content.anchoredPosition);
position += offset;
}
if (!m_Horizontal)
position.x = m_Content.anchoredPosition.x;
if (!m_Vertical)
position.y = m_Content.anchoredPosition.y;
if (position != m_Content.anchoredPosition)
m_Content.anchoredPosition = position;
}
}
if (m_Dragging && m_Inertia)
{
Vector3 newVelocity = (m_Content.anchoredPosition - m_PrevPosition) / deltaTime;
m_Velocity = Vector3.Lerp (m_Velocity, newVelocity, deltaTime * 10);
}
UpdateBounds ();
if (m_ViewBounds != m_PrevViewBounds || m_ContentBounds != m_PrevContentBounds || m_Content.anchoredPosition != m_PrevPosition)
{
UpdateScrollbars (offset);
m_OnValueChanged.Invoke (normalizedPosition);
UpdatePrevData ();
}
}
void UpdatePrevData ()
{
if (m_Content == null)
m_PrevPosition = Vector2.zero;
else
m_PrevPosition = m_Content.anchoredPosition;
m_PrevViewBounds = m_ViewBounds;
m_PrevContentBounds = m_ContentBounds;
}
void UpdateScrollbars (Vector2 offset)
{
if (m_HorizontalScrollbar)
{
m_HorizontalScrollbar.size = Mathf.Clamp01 ((m_ViewBounds.size.x - Mathf.Abs (offset.x)) / m_ContentBounds.size.x);
m_HorizontalScrollbar.value = horizontalNormalizedPosition;
}
if (m_VerticalScrollbar)
{
m_VerticalScrollbar.size = Mathf.Clamp01 ((m_ViewBounds.size.y - Mathf.Abs (offset.y)) / m_ContentBounds.size.y);
m_VerticalScrollbar.value = verticalNormalizedPosition;
}
}
public Vector2 normalizedPosition
{
get
{
return new Vector2 (horizontalNormalizedPosition, verticalNormalizedPosition);
}
set
{
SetHorizontalNormalizedPosition (value.x);
SetVerticalNormalizedPosition (value.y);
}
}
public float horizontalNormalizedPosition
{
get
{
if (m_ContentBounds.size.x <= m_ViewBounds.size.x)
return (m_ViewBounds.min.x > m_ContentBounds.min.x) ? 1 : 0;
return Mathf.Clamp01 ((m_ViewBounds.min.x - m_ContentBounds.min.x) / (m_ContentBounds.size.x - m_ViewBounds.size.x));
}
set
{
SetHorizontalNormalizedPosition (value);
}
}
public float verticalNormalizedPosition
{
get
{
if (m_ContentBounds.size.y <= m_ViewBounds.size.y)
return (m_ViewBounds.min.y > m_ContentBounds.min.y) ? 1 : 0;;
return Mathf.Clamp01 ((m_ViewBounds.min.y - m_ContentBounds.min.y) / (m_ContentBounds.size.y - m_ViewBounds.size.y));
}
set
{
SetVerticalNormalizedPosition (value);
}
}
void SetHorizontalNormalizedPosition (float value)
{
UpdateBounds ();
float scroll = m_ViewBounds.min.x - value * (m_ContentBounds.size.x - m_ViewBounds.size.x);
Vector2 anchoredPosition = m_Content.anchoredPosition;
anchoredPosition.x += scroll - m_ContentBounds.min.x;
if (Mathf.Abs (m_Content.anchoredPosition.x - anchoredPosition.x) > 0.01f)
{
m_Content.anchoredPosition = anchoredPosition;
m_Velocity.x = 0;
}
}
void SetVerticalNormalizedPosition (float value)
{
UpdateBounds ();
float scroll = m_ViewBounds.min.y - value * (m_ContentBounds.size.y - m_ViewBounds.size.y);
Vector2 anchoredPosition = m_Content.anchoredPosition;
anchoredPosition.y += scroll - m_ContentBounds.min.y;
if (Mathf.Abs (m_Content.anchoredPosition.y - anchoredPosition.y) > 0.01f)
{
m_Content.anchoredPosition = anchoredPosition;
m_Velocity.y = 0;
}
}
static float RubberDelta (float overStretching, float viewSize)
{
return (1 - (1 / ((Mathf.Abs (overStretching) * 0.55f / viewSize) + 1))) * viewSize * Mathf.Sign (overStretching);
}
void UpdateBounds ()
{
m_ViewBounds = new Bounds (m_ViewRect.rect.center, m_ViewRect.rect.size);
m_ContentBounds = GetBounds();
if (m_Content == null)
return;
// Make sure content bounds are at laeast as large as view by adding padding if not.
// One might think at first that if the content is smaller than the view, scrolling should be allowed.
// However, that's not how scroll views normally work.
// Scrolling is *only* possible when content is *larger* than view.
// We use the pivot of the content rect to decide in which directions the content bounds should be expanded.
// E.g. if pivot is at top, bounds are expanded downwards.
// This also works nicely when ContentSizeFitter is used on the content.
Vector3 contentSize = m_ContentBounds.size;
Vector3 contentPos = m_ContentBounds.center;
Vector3 excess = m_ViewBounds.size - contentSize;
if (horizontal && excess.x > 0)
{
contentPos.x -= excess.x * (m_Content.pivot.x - 0.5f);
contentSize.x = m_ViewBounds.size.x;
}
if (vertical && excess.y > 0)
{
contentPos.y -= excess.y * (m_Content.pivot.y - 0.5f);
contentSize.y = m_ViewBounds.size.y;
}
m_ContentBounds.size = contentSize;
m_ContentBounds.center = contentPos;
}
private readonly Vector3[] m_Corners = new Vector3[4];
private Bounds GetBounds()
{
if (m_Content == null)
return new Bounds();
var vMin = new Vector3 (float.MaxValue, float.MaxValue, float.MaxValue);
var vMax = new Vector3 (float.MinValue, float.MinValue, float.MinValue);
var toLocal = m_ViewRect.worldToLocalMatrix;
m_Content.GetWorldCorners (m_Corners);
for (int j = 0; j < 4; j++)
{
Vector3 v = toLocal.MultiplyPoint3x4 (m_Corners[j]);
vMin = Vector3.Min (v, vMin);
vMax = Vector3.Max (v, vMax);
}
var bounds = new Bounds (vMin, Vector3.zero);
bounds.Encapsulate (vMax);
return bounds;
}
Vector3 CalculateOffset (Vector2 delta)
{
Vector3 offset = Vector3.zero;
if (m_MovementType == MovementType.Unrestricted)
return offset;
Vector2 min = m_ContentBounds.min;
Vector2 max = m_ContentBounds.max;
if (m_Horizontal)
{
min.x += delta.x;
max.x += delta.x;
if (min.x > m_ViewBounds.min.x)
offset.x = m_ViewBounds.min.x - min.x;
else if (max.x < m_ViewBounds.max.x)
offset.x = m_ViewBounds.max.x - max.x;
}
if (m_Vertical)
{
min.y += delta.y;
max.y += delta.y;
if (max.y < m_ViewBounds.max.y)
offset.y = m_ViewBounds.max.y - max.y;
else if (min.y > m_ViewBounds.min.y)
offset.y = m_ViewBounds.min.y - min.y;
}
return offset;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment