资料
ScrollRect源码
创建Scroll View,选择组件对象,查看源码
作用
实现滑动窗口,学习滑动窗口如何实现。
思路
- 继承IDragHandler, IBeginDragHandler, IEndDragHandler,实现拖拽功能
- 计算可视区域和移动区域的Bounds,限制移动区域
实现
主要方法:
- UpdateBounds方法用于计算可视区域和内容区域的包围盒
- AdjuseBounds方法调节内容区域的包围盒
- CalculateOffset方法计算偏移位置,防止超出边界
csharp
using UnityEngine;
using UnityEngine.EventSystems;
public class SimpleScrollRect : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler
{
[Header("拖动区域")]
[SerializeField] RectTransform viewRect;
[Header("移动内容")]
[SerializeField] RectTransform content;
[Header("水平移动")]
[SerializeField] bool horizontal;
[Header("竖直移动")]
[SerializeField] bool vertical;
Vector2 startLocalCursor;
Vector2 contentStartPosition;
bool isDrag = false;
Bounds viewBounds;
Bounds contentBounds;
public void OnBeginDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
RectTransformUtility.ScreenPointToLocalPointInRectangle(viewRect,
eventData.position, eventData.pressEventCamera,
out startLocalCursor);
contentStartPosition = content.anchoredPosition;
isDrag = true;
}
public void OnDrag(PointerEventData eventData)
{
if (isDrag == false) return;
if (eventData.button != PointerEventData.InputButton.Left)
return;
Vector2 localCursor;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(viewRect,
eventData.position, eventData.pressEventCamera, out localCursor))
return;
UpdateBounds();
//从拖拽开始到当前帧,鼠标在 viewRect 局部坐标系中的总累计位移
var pointerDelta = localCursor - startLocalCursor;
//目标位置
var position = contentStartPosition + pointerDelta;
//从当前实际位置到目标位置所需的位移
var delta = position - content.anchoredPosition;
//边界限制
var offset = CalculateOffset(ref viewBounds, ref contentBounds, horizontal, vertical, delta);
position += offset;
//水平或竖直移动限制
if (!horizontal)
position.x = content.anchoredPosition.x;
if (!vertical)
position.y = content.anchoredPosition.y;
//更新位置
if (position != content.anchoredPosition)
content.anchoredPosition = position;
}
private readonly Vector3[] contentCorners = new Vector3[4];
protected void UpdateBounds()
{
//生成可视区域bounds
viewBounds = new Bounds(viewRect.rect.center, viewRect.rect.size);
//生成内容区域Bounds,坐标系统一在viewRect的局部坐标系中
var viewWorldToLocalMatrix = viewRect.worldToLocalMatrix;
content.GetWorldCorners(contentCorners);
contentBounds = GetBounds(contentCorners, ref viewWorldToLocalMatrix);
//调整内容区域Bounds,确保内容区域永远不会小于可视区域
Vector3 contentSize = contentBounds.size;
Vector3 contentPos = contentBounds.center;
var contentPivot = content.pivot;
AdjustBounds(ref viewBounds, ref contentPivot, ref contentSize, ref contentPos);
contentBounds.size = contentSize;
contentBounds.center = contentPos;
}
private Bounds GetBounds(Vector3[] corners, ref Matrix4x4 viewWorldToLocalMatrix)
{
var vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
var vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
for (int j = 0; j < 4; j++)
{
Vector3 v = viewWorldToLocalMatrix.MultiplyPoint3x4(corners[j]);
vMin = Vector3.Min(v, vMin);
vMax = Vector3.Max(v, vMax);
}
var bounds = new Bounds(vMin, Vector3.zero);
bounds.Encapsulate(vMax);
return bounds;
}
private void AdjustBounds(ref Bounds viewBounds, ref Vector2 contentPivot, ref Vector3 contentSize, ref Vector3 contentPos)
{
Vector3 excess = viewBounds.size - contentSize;//可视区域超出内容区域的尺寸
if (excess.x > 0)//当内容区域宽度小于可视区域宽度,扩展内容区域宽度
{
//contentPivot.x=0.5 原有矩形中心位置不变,宽度向两侧增加,中心点不改变
//contentPivto.x=0 原有矩形左边界位置不变,宽度向右侧增加,中心点向右移动
//contentPivot.x=1 原有矩形右边界位置不变,宽度向左测增加,中心点向左移动
//宽度改变时,改变中心点,保证pivot的视觉位置不变
contentPos.x -= excess.x * (contentPivot.x - 0.5f);
contentSize.x = viewBounds.size.x;
}
if (excess.y > 0)//当内容区域高度小于可视区域高度,扩展内容区域高度
{
contentPos.y -= excess.y * (contentPivot.y - 0.5f);
contentSize.y = viewBounds.size.y;
}
}
private Vector2 CalculateOffset(ref Bounds viewBounds, ref Bounds contentBounds, bool horizontal, bool vertical, Vector2 delta)
{
Vector2 offset = Vector2.zero;
Vector2 min = contentBounds.min;
Vector2 max = contentBounds.max;
if (horizontal)
{
min.x += delta.x;//模拟内容偏移后的左边界
max.x += delta.x;//模拟内容偏移后的右边界
float minOffset = viewBounds.min.x - min.x;//可视区域的左边界减去内容左边界
//>0 内容左边界 在 可视区域左边界 左侧
//<0 内容左边界 在 可视区域左边界 右侧
float maxOffset = viewBounds.max.x - max.x;//可视区域的右边界减去内容右边界
//>0 内容右边界 在 可视区域右边界 左侧
//<0 内容右边界 在 可视区域右边界 右侧
if (minOffset < -0.001f) //左边界对齐
offset.x = minOffset;
else if (maxOffset > 0.001f)//右边界对齐
offset.x = maxOffset;
}
if (vertical)
{
min.y += delta.y;//模拟内容偏移后的下边界
max.y += delta.y;//模拟内容偏移后的上边界
float maxOffset = viewBounds.max.y - max.y;//可视区域的上边界减去内容上边界
//>0 内容上边界 在 可视区域上边界 下侧
//<0 内容上边界 在 可视区域上边界 上侧
float minOffset = viewBounds.min.y - min.y;//可视区域的下边界减去内容下边界
//>0 内容下边界 在 可视区域下边界 下侧
//<0 内容下边界 在 可视区域下边界 上侧
if (maxOffset > 0.001f)//上边界对齐
offset.y = maxOffset;
else if (minOffset < -0.001f)//下边界对齐
offset.y = minOffset;
}
return offset;
}
public void OnEndDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
isDrag = false;
}
}