Created
September 22, 2014 09:55
-
-
Save stramit/b5f41bf07a3947ac4a73 to your computer and use it in GitHub Desktop.
Scroll Rect
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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