using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using System; using Cysharp.Threading.Tasks; using System.Diagnostics; [DisallowMultipleComponent] public class CyclicScroll : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { [SerializeField] RectTransform m_Content; public RectTransform content { get { return m_Content; } } [SerializeField] protected BoundOffset m_BoundOffset; [SerializeField] protected Vector2 m_CellSize = new Vector2(100, 100); public Vector2 cellSize { get { return m_CellSize; } } [SerializeField] protected Vector2 m_Spacing; public Vector2 spacing { get { return m_Spacing; } } [SerializeField] Align m_Align = Align.Top; public Align align { get { return m_Align; } } [SerializeField] bool m_ElasticityOn = true; public bool elasticityOn { get { return m_ElasticityOn; } } [SerializeField] private float m_Elasticity = 0.1f; public float elasticity { get { return m_Elasticity; } } [SerializeField] private float m_DecelerationRate = 0.135f; public float decelerationRate { get { return m_DecelerationRate; } } [SerializeField] string m_ChildPrefabName = string.Empty; [SerializeField] string m_ChildDisplayName = string.Empty; [SerializeField] int m_ChildCount = 5; public float normalizedPosition { get { if (content == null) { return 0f; } var offset = (dataMaxOffset - dataMinOffset); switch (align) { case Align.Top: case Align.Bottom: return Mathf.Clamp01(Mathf.Abs((content.anchoredPosition.y - dataMinOffset.y) / offset.y)); case Align.Left: case Align.Right: return Mathf.Clamp01(Mathf.Abs((content.anchoredPosition.x - dataMinOffset.x) / offset.x)); default: return 0f; } } set { if (content == null) { return; } value = Mathf.Clamp01(value); var normalize = 0f; var offset = (dataMaxOffset - dataMinOffset); switch (align) { case Align.Top: case Align.Bottom: normalize = (content.anchoredPosition.y - dataMinOffset.y) / offset.y; if (Mathf.Abs(normalize - value) > 0.0001f) { targetOffset = dataMinOffset + new Vector2(0, dataMaxOffset.y * value); velocity = 0f; autoLerp = true; refAutoLerpPosition = Vector2.zero; } break; case Align.Left: case Align.Right: normalize = (content.anchoredPosition.x - dataMinOffset.x) / offset.x; if (Mathf.Abs(normalize - value) > 0.0001f) { targetOffset = dataMinOffset + new Vector2(dataMaxOffset.x * value, 0); velocity = 0f; autoLerp = true; refAutoLerpPosition = Vector2.zero; } break; } } } protected Vector2 dataMaxOffset = Vector2.zero; protected Vector2 dataMinOffset = Vector2.zero; public bool autoLerp { get; private set; } Vector2 targetOffset = Vector2.zero; Vector2 refAutoLerpPosition = Vector2.zero; protected IList datas; protected List infiniteItems = new List(); List tempList = new List(); Vector2 startMousePosition = Vector2.zero; Vector2 startContentPosition = Vector2.zero; Vector2 prevPosition = Vector2.zero; protected float velocity = 0f; protected bool dragging = false; public int dataCount { get { return datas == null ? 0 : datas.Count; } } bool moveNextable { get { return hostIndex < dataCount - 1; } } bool moveLastable { get { return preIndex > 0; } } public RectTransform rectTransform { get { return this.transform as RectTransform; } } private Vector2 maxOffset { get { return rectTransform.GetMaxReferencePosition(rectTransform); } } private Vector2 minOffset { get { return rectTransform.GetMinReferencePosition(rectTransform); } } protected int preIndex { get; set; } protected int hostIndex { get; set; } public void LuaInit(int[] indexs, bool stepByStep) { Init(new List(indexs), stepByStep); } public virtual void Init(List _datas, bool _stepByStep = false) { if (_datas == null) { this.SetActive(false); return; } datas = _datas; ReArrange(); FillBatchData(0); if (_stepByStep) { for (int i = 0; i < infiniteItems.Count; i++) { var infiniteItem = infiniteItems[i]; if (infiniteItem != null) { infiniteItem.SetActive(false); } } } this.SetActive(true); dataMinOffset = content.anchoredPosition; var totalOffset = dataCount == 0 ? Vector2.zero : cellSize * dataCount + spacing * (dataCount - 1) + new Vector2(m_BoundOffset.left + m_BoundOffset.right, m_BoundOffset.top + m_BoundOffset.bottom); var longer = Mathf.Abs(totalOffset.x) > Mathf.Abs(rectTransform.rect.width); var higher = Mathf.Abs(totalOffset.y) > Mathf.Abs(rectTransform.rect.height); switch (align) { case Align.Left: dataMaxOffset = longer ? dataMinOffset - new Vector2(totalOffset.x - rectTransform.rect.width, 0) : dataMinOffset; break; case Align.Right: dataMaxOffset = longer ? dataMinOffset + new Vector2(totalOffset.x - rectTransform.rect.width, 0) : dataMinOffset; break; case Align.Top: dataMaxOffset = higher ? dataMinOffset + new Vector2(0, totalOffset.y - rectTransform.rect.height) : dataMinOffset; break; case Align.Bottom: dataMaxOffset = higher ? dataMinOffset - new Vector2(0, totalOffset.y - rectTransform.rect.height) : dataMinOffset; break; } if (_stepByStep) { Co_StepByStepAppear(); // StartCoroutine("Co_StepByStepAppear"); } } public void Dispose() { velocity = 0f; StopAllCoroutines(); for (int i = 0; i < infiniteItems.Count; i++) { var infiniteItem = infiniteItems[i]; if (infiniteItem != null) { infiniteItem.Dispose(); } } } public void HideAll() { StopAllCoroutines(); for (int i = 0; i < content.childCount; i++) { content.GetChild(i).SetActive(false); } } private async UniTask Co_StepByStepAppear() { for (int i = 0; i < infiniteItems.Count; i++) { var infiniteItem = infiniteItems[i]; if (infiniteItem != null && i < datas.Count) { infiniteItem.SetActive(true); infiniteItem.OpeningShow(); await UniTask.Delay(1000); } } } public void MoveToCenter(int _dataIndex) { if (_dataIndex < 0 || _dataIndex >= dataCount) { return; } switch (align) { case Align.Left: var leftOffsetX = Mathf.Clamp(m_BoundOffset.left + (_dataIndex + 0.5f) * cellSize.x + _dataIndex * spacing.x - rectTransform.rect.width * 0.5f, 0, Mathf.Abs(dataMaxOffset.x)); targetOffset = new Vector2(dataMinOffset.x - leftOffsetX, 0); break; case Align.Right: var rightOffsetX = Mathf.Clamp(m_BoundOffset.right + (_dataIndex + 0.5f) * cellSize.x + _dataIndex * spacing.x - rectTransform.rect.width * 0.5f, 0, Mathf.Abs(dataMaxOffset.x)); targetOffset = new Vector2(dataMinOffset.x + rightOffsetX, 0); break; case Align.Top: var topOffsetY = Mathf.Clamp(m_BoundOffset.top + (_dataIndex + 0.5f) * cellSize.y + _dataIndex * spacing.y - rectTransform.rect.height * 0.5f, 0, Mathf.Abs(dataMaxOffset.y)); targetOffset = new Vector2(0, dataMinOffset.y + topOffsetY); break; case Align.Bottom: var bottomOffsetY = Mathf.Clamp(m_BoundOffset.bottom + (_dataIndex + 0.5f) * cellSize.y + _dataIndex * spacing.y - rectTransform.rect.height * 0.5f, 0, Mathf.Abs(dataMaxOffset.y)); targetOffset = new Vector2(0, dataMinOffset.y - bottomOffsetY); break; } autoLerp = true; } #region Drag Process public void OnBeginDrag(PointerEventData eventData) { if (autoLerp || eventData.button != PointerEventData.InputButton.Left) { return; } startMousePosition = Vector2.zero; RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, eventData.pressEventCamera, out startMousePosition); prevPosition = startContentPosition = content.anchoredPosition; velocity = 0f; dragging = true; } public void OnDrag(PointerEventData eventData) { // if (NewBieCenter.Instance.inGuiding) // { // return; // } if (autoLerp || eventData.button != PointerEventData.InputButton.Left) { return; } var localMouse = new Vector2(); if (RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, eventData.position, eventData.pressEventCamera, out localMouse)) { var pointerDelta = localMouse - startMousePosition; var position = startContentPosition + pointerDelta; SetContentAnchoredPosition(position, align); } } public void OnEndDrag(PointerEventData eventData) { if (autoLerp || eventData.button != PointerEventData.InputButton.Left) { return; } dragging = false; } #endregion private void Update() { if (autoLerp && Vector2.Distance(content.anchoredPosition, targetOffset) > 0.1f) { var newPosition = Vector2.SmoothDamp(content.anchoredPosition, targetOffset, ref refAutoLerpPosition, m_Elasticity, Mathf.Infinity, Time.unscaledDeltaTime); SetContentAnchoredPosition(newPosition, align); } else { autoLerp = false; } } protected virtual void LateUpdate() { if (content == null) { return; } var deltaTime = Time.unscaledDeltaTime; var offset = CalculateOffset(align); if (!dragging && (velocity != 0f || Vector2.SqrMagnitude(offset - Vector2.zero) > 0.1f)) { Vector2 position = content.anchoredPosition; if (Vector2.SqrMagnitude(offset - Vector2.zero) > 0.1f) { float speed = velocity; switch (align) { case Align.Left: case Align.Right: position[0] = Mathf.SmoothDamp(content.anchoredPosition[0], content.anchoredPosition[0] - offset.x, ref speed, m_Elasticity, Mathf.Infinity, deltaTime); break; case Align.Bottom: case Align.Top: position[1] = Mathf.SmoothDamp(content.anchoredPosition[1], content.anchoredPosition[1] - offset.y, ref speed, m_Elasticity, Mathf.Infinity, deltaTime); break; } if (Mathf.Abs(speed) < 1) { speed = 0f; } velocity = speed; } else { velocity *= Mathf.Pow(m_DecelerationRate, deltaTime); if (Mathf.Abs(velocity) < 50) { velocity = 0; } switch (align) { case Align.Left: case Align.Right: position[0] += velocity * deltaTime; break; case Align.Bottom: case Align.Top: position[1] += velocity * deltaTime; break; } } SetContentAnchoredPosition(position, align); } if (dragging /*&& content.anchoredPosition != prevPosition*/) { var newVelocity = 0f; switch (align) { case Align.Left: case Align.Right: newVelocity = (content.anchoredPosition[0] - prevPosition[0]) / deltaTime; break; case Align.Bottom: case Align.Top: newVelocity = (content.anchoredPosition[1] - prevPosition[1]) / deltaTime; break; } velocity = Mathf.Lerp(velocity, newVelocity, deltaTime * 10); prevPosition = content.anchoredPosition; } } protected virtual void ProcessMoveNext() { ScrollItem lastRect = null; ScrollItem item = null; tempList.Clear(); for (int i = 0; i < infiniteItems.Count; i++) { tempList.Add(infiniteItems[i]); } if (tempList.Count <= 0) { return; } lastRect = tempList[tempList.Count - 1]; for (int i = 0; i < tempList.Count; i++) { item = tempList[i]; var able = moveNextable && TestMoveNext(align, item); if (able) { infiniteItems.Remove(item); infiniteItems.Add(item); var offset = CalculateElementOffset(align); item.rectTransform.anchoredPosition = lastRect.rectTransform.anchoredPosition + offset; lastRect = item; hostIndex++; preIndex++; item.Dispose(); item.LuaDispose(); item.Display(datas[hostIndex]); item.LuaDisplay(hostIndex); } else { break; } } } protected virtual void ProcessMoveLast() { ScrollItem firstRect = null; ScrollItem item = null; tempList.Clear(); for (int i = 0; i < infiniteItems.Count; i++) { tempList.Add(infiniteItems[i]); } firstRect = tempList[0]; for (int i = tempList.Count - 1; i >= 0; i--) { item = tempList[i]; var able = moveLastable && TestMoveLast(align, item); if (able) { infiniteItems.Remove(item); infiniteItems.Insert(0, item); var offset = CalculateElementOffset(align); item.rectTransform.anchoredPosition = firstRect.rectTransform.anchoredPosition - offset; firstRect = item; hostIndex--; preIndex--; item.Dispose(); item.LuaDispose(); item.Display(datas[preIndex]); item.LuaDisplay(preIndex); } else { break; } } } private void SetContentAnchoredPosition(Vector2 _position, Align _align) { if (_position == content.anchoredPosition) { return; } if (!elasticityOn) { _position.y = Mathf.Clamp(_position.y, dataMinOffset.y, dataMaxOffset.y); _position.x = Mathf.Clamp(_position.x, dataMinOffset.x, dataMaxOffset.x); } var offset = _position - content.anchoredPosition; switch (_align) { case Align.Left: case Align.Right: _position[1] = content.anchoredPosition[1]; content.anchoredPosition = _position; break; case Align.Top: case Align.Bottom: _position[0] = content.anchoredPosition[0]; content.anchoredPosition = _position; break; } var moveNext = (_align == Align.Left && offset.x < 0f) || (_align == Align.Right && offset.x > 0f) || (_align == Align.Top && offset.y > 0f) || (_align == Align.Bottom && offset.y < 0f); if (moveNext) { ProcessMoveNext(); } else { ProcessMoveLast(); } } private Vector2 CalculateOffset(Align _align) { var offset = Vector2.zero; switch (_align) { case Align.Left: if (content.anchoredPosition.x > dataMinOffset.x) { offset = content.anchoredPosition - dataMinOffset; } else if (content.anchoredPosition.x < dataMaxOffset.x) { offset = content.anchoredPosition - dataMaxOffset; } break; case Align.Right: if (content.anchoredPosition.x < dataMinOffset.x) { offset = content.anchoredPosition - dataMinOffset; } else if (content.anchoredPosition.x > dataMaxOffset.x) { offset = content.anchoredPosition - dataMaxOffset; } break; case Align.Bottom: if (content.anchoredPosition.y > dataMinOffset.y) { offset = content.anchoredPosition - dataMinOffset; } else if (content.anchoredPosition.y < dataMaxOffset.y) { offset = content.anchoredPosition - dataMaxOffset; } break; case Align.Top: if (content.anchoredPosition.y < dataMinOffset.y) { offset = content.anchoredPosition - dataMinOffset; } else if (content.anchoredPosition.y > dataMaxOffset.y) { offset = content.anchoredPosition - dataMaxOffset; } break; } return offset; } [ContextMenu("Arrange")] public virtual void ReArrange() { velocity = 0f; autoLerp = false; CreateElements(); ElementsMatch(); Arrange(align); } private void CreateElements() { var items = this.content.GetComponentsInChildren(true); if (items.Length < m_ChildCount) { var dif = m_ChildCount - items.Length; if (!string.IsNullOrEmpty(m_ChildPrefabName) && !string.IsNullOrEmpty(m_ChildDisplayName)) { for (var i = 0; i < dif; i++) { var instance = UIUtility.CreateWidget(m_ChildPrefabName, m_ChildDisplayName); instance.transform.SetParentEx(this.m_Content, Vector3.zero, Quaternion.identity, Vector3.one); } } } } private void Arrange(Align _align) { var head = infiniteItems[0]; var offset1 = Vector2.zero; switch (_align) { case Align.Left: offset1 = new Vector2(-content.rect.width * 0.5f + head.rectTransform.rect.width * 0.5f + m_BoundOffset.left, 0); break; case Align.Right: offset1 = new Vector2(content.rect.width * 0.5f - head.rectTransform.rect.width * 0.5f - m_BoundOffset.right, 0); break; case Align.Top: offset1 = new Vector2(0f, content.rect.height * 0.5f - head.rectTransform.rect.height * 0.5f - m_BoundOffset.top); break; case Align.Bottom: offset1 = new Vector2(0f, -content.rect.height * 0.5f + head.rectTransform.rect.height * 0.5f + m_BoundOffset.bottom); break; } head.rectTransform.anchoredPosition = content.anchoredPosition + offset1; var offset2 = CalculateElementOffset(_align); for (int i = 1; i < infiniteItems.Count; i++) { var item = infiniteItems[i]; item.rectTransform.anchoredPosition = head.rectTransform.anchoredPosition + offset2 * i; } } private void ElementsMatch() { if (content == null) { UnityEngine.Debug.Log("Content 不能为空!"); return; } infiniteItems.Clear(); for (int i = 0; i < content.childCount; i++) { var infiniteItem = content.GetChild(i).GetComponent(); if (infiniteItem != null) { infiniteItems.Add(infiniteItem); infiniteItem.rectTransform.sizeDelta = cellSize; infiniteItem.rectTransform.anchorMax = Vector2.one * 0.5f; infiniteItem.rectTransform.anchorMin = Vector2.one * 0.5f; infiniteItem.rectTransform.pivot = Vector2.one * 0.5f; } } content.anchorMax = content.anchorMin = content.pivot = Vector2.one * 0.5f; content.sizeDelta = rectTransform.sizeDelta; content.anchoredPosition = Vector2.zero; } private void FillBatchData(int _startIndex) { int min = Mathf.Min(infiniteItems.Count, dataCount); preIndex = Mathf.Clamp(_startIndex, 0, dataCount - min); hostIndex = preIndex + min - 1; for (int i = 0; i < infiniteItems.Count; i++) { var item = infiniteItems[i]; if (i <= hostIndex - preIndex) { item.SetActive(true); item.Display(datas[preIndex + i]); item.LuaDisplay(preIndex + i); } else { item.SetActive(false); item.Dispose(); item.LuaDispose(); } } } private Vector2 CalculateElementOffset(Align _align) { switch (_align) { case Align.Left: return new Vector2(cellSize.x + spacing.x, 0); case Align.Right: return new Vector2(-cellSize.x - spacing.x, 0); case Align.Top: return new Vector2(0, -cellSize.y - spacing.y); case Align.Bottom: return new Vector2(0, cellSize.y + spacing.y); default: return Vector2.zero; } } private bool TestMoveNext(Align _align, ScrollItem _item) { var itemMinPosition = _item.rectTransform.GetMinReferencePosition(rectTransform); var itemMaxPosition = _item.rectTransform.GetMaxReferencePosition(rectTransform); switch (_align) { case Align.Left: return itemMaxPosition.x < minOffset.x; case Align.Bottom: return itemMaxPosition.y < minOffset.y; case Align.Right: return itemMinPosition.x > maxOffset.x; case Align.Top: return itemMinPosition.y > maxOffset.y; default: return false; } } private bool TestMoveLast(Align _align, ScrollItem _item) { var itemMinPosition = _item.rectTransform.GetMinReferencePosition(rectTransform); var itemMaxPosition = _item.rectTransform.GetMaxReferencePosition(rectTransform); switch (_align) { case Align.Left: return itemMinPosition.x > maxOffset.x; case Align.Bottom: return itemMinPosition.y > maxOffset.y; case Align.Right: return itemMaxPosition.x < minOffset.x; case Align.Top: return itemMaxPosition.y < minOffset.y; default: return false; } } public enum Align { Left, Right, Top, Bottom, } [Serializable] public struct BoundOffset { public float left; public float right; public float top; public float bottom; } }