using System.Net; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using System; using System.Collections; using System.Collections.Generic; using UnityEngine.UI; namespace BF { [DisallowMultipleComponent] [RequireComponent(typeof(RectTransform))] public abstract class ScrollRectBaseOld : UIBehaviour, IInitializePotentialDragHandler, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler, ICanvasElement, ILayoutElement, ILayoutGroup { 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 } public enum ScrollbarVisibility { Permanent, AutoHide, AutoHideAndExpandViewport, } #region ----------------------- Expansion Field ------------------------- public BFCell bfCell; public bool useBar = false; private int totalCount; //Total cell count, negative means infinite loop mode protected float startThreshold = -1; protected float endThreshold = -1; // [Tooltip("Reverse direction for dragging")] // public bool reverseDirection = false; [Tooltip("Rubber scale for outside")] public float rubberScale = 1; protected abstract float GetSize(RectTransform item); protected abstract float GetDimension(Vector2 vector); protected abstract Vector2 GetVector(float value); protected int directionSign = 0; // horizontal will be 1 || vertical will be -1 protected GridLayoutGroup gridLayout = null; private float contentSpacing = -1; protected float ContentSpacing { get { if (contentSpacing >= 0) return contentSpacing; contentSpacing = 0; if (content != null) { HorizontalOrVerticalLayoutGroup layout1 = content.GetComponent(); if (layout1 != null) contentSpacing = layout1.spacing; gridLayout = content.GetComponent(); if (gridLayout != null) contentSpacing = Mathf.Abs(GetDimension(gridLayout.spacing)); } return contentSpacing; } } private int contentConstraintCount = 0; protected int ContentConstraintCount { get { if (contentConstraintCount > 0) return contentConstraintCount; contentConstraintCount = 1; if (content != null) { GridLayoutGroup layout2 = content.GetComponent(); if (layout2 != null) { if (layout2.constraint == GridLayoutGroup.Constraint.Flexible) Debug.LogWarning("[LoopScrollRect] Flexible not supported yet"); contentConstraintCount = layout2.constraintCount; } } return contentConstraintCount; } } [SerializeField] public Action onValueChanged; RectTransform cacheRoot;//缓存root bool initedCacheRoot; List cachedCellList = new List();//缓存cellObj private List allCells = new List(); private int objectIndex; protected int curTopIndex = 0; protected int curBottomIndex = 0; protected int cellStartIndex = 0; protected int cellEndIndex = 0; protected int selectedIndex = 0; public Action luaInstantiateCellAction; public Action luaRefreshAction; public Action luaSetSelectedAction; #endregion #region ----------------------- Original Field ------------------------- public RectTransform content; public bool horizontal = true; public bool vertical = true; public MovementType movementType = MovementType.Elastic; public float elasticity = 0.1f; // Only used for MovementType.Elastic public bool inertia = true; public float decelerationRate = 0.135f; // Only used when inertia is enabled public float scrollSensitivity = 1.0f; public RectTransform viewport; private bool scrolling = false; // The offset from handle position to mouse down position private Vector2 pointerStartLocalCursor = Vector2.zero; private Vector2 contentStartPosition = Vector2.zero; private Bounds contentBounds; private Bounds viewBounds; public Vector2 velocity; private bool dragging; private Vector2 prevPosition = Vector2.zero; private Bounds prevContentBounds; private Bounds prevViewBounds; [NonSerialized] private bool hasRebuiltLayout = false; [NonSerialized] private RectTransform rectTrans; private RectTransform RectTrans { get { if (rectTrans == null) rectTrans = GetComponent(); return rectTrans; } } private DrivenRectTransformTracker tracker; #endregion #region ----------------------- ScrollBar ------------------------- [SerializeField] Scrollbar horizontalScrollbar; public Scrollbar HorizontalScrollbar { get { return horizontalScrollbar; } set { if (horizontalScrollbar) horizontalScrollbar.onValueChanged.RemoveListener(SetHorizontalNormalizedPosition); horizontalScrollbar = value; if (horizontalScrollbar) horizontalScrollbar.onValueChanged.AddListener(SetHorizontalNormalizedPosition); SetDirtyCaching(); } } [SerializeField] Scrollbar verticalScrollbar; public Scrollbar VerticalScrollbar { get { return verticalScrollbar; } set { if (verticalScrollbar) verticalScrollbar.onValueChanged.RemoveListener(SetVerticalNormalizedPosition); verticalScrollbar = value; if (verticalScrollbar) verticalScrollbar.onValueChanged.AddListener(SetVerticalNormalizedPosition); SetDirtyCaching(); } } [SerializeField] ScrollbarVisibility horizontalScrollbarVisibility; public ScrollbarVisibility HorizontalScrollbarVisibility { get { return horizontalScrollbarVisibility; } set { horizontalScrollbarVisibility = value; SetDirtyCaching(); } } [SerializeField] ScrollbarVisibility verticalScrollbarVisibility; public ScrollbarVisibility VerticalScrollbarVisibility { get { return verticalScrollbarVisibility; } set { verticalScrollbarVisibility = value; SetDirtyCaching(); } } [SerializeField] float horizontalScrollbarSpacing; public float HorizontalScrollbarSpacing { get { return horizontalScrollbarSpacing; } set { horizontalScrollbarSpacing = value; SetDirty(); } } [SerializeField] float verticalScrollbarSpacing; public float VerticalScrollbarSpacing { get { return verticalScrollbarSpacing; } set { verticalScrollbarSpacing = value; SetDirty(); } } bool hSliderExpand; bool vSliderExpand; float hSliderHeight; float vSliderWidth; RectTransform horizontalScrollbarRect; RectTransform verticalScrollbarRect; #endregion protected ScrollRectBaseOld() { flexibleWidth = -1; } protected override void OnEnable() { base.OnEnable(); if (useBar) { if (horizontalScrollbar) horizontalScrollbar.onValueChanged.AddListener(SetHorizontalNormalizedPosition); if (verticalScrollbar) verticalScrollbar.onValueChanged.AddListener(SetVerticalNormalizedPosition); } CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this); } protected override void OnDisable() { CanvasUpdateRegistry.UnRegisterCanvasElementForRebuild(this); if (useBar) { if (horizontalScrollbar) horizontalScrollbar.onValueChanged.RemoveListener(SetHorizontalNormalizedPosition); if (verticalScrollbar) verticalScrollbar.onValueChanged.RemoveListener(SetVerticalNormalizedPosition); } hasRebuiltLayout = false; tracker.Clear(); velocity = Vector2.zero; LayoutRebuilder.MarkLayoutForRebuild(RectTrans); base.OnDisable(); } protected override void Awake() { if (!initedCacheRoot) InitCacheRoot(); } void InitCacheRoot() { var cacheTransform = transform.Find(StringConst.CACHE_ROOT_NAME); if (cacheTransform == null) { GameObject go = new GameObject(StringConst.CACHE_ROOT_NAME); cacheRoot = go.AddComponent(); cacheRoot.SetParent(transform); cacheRoot.localPosition = Vector3.zero; go.SetActive(false); } if (cacheRoot == null) cacheRoot = cacheTransform.transform as RectTransform; initedCacheRoot = true; } protected override void OnDestroy() { luaInstantiateCellAction = null; } void CacheCell(CellObj cell) { if (cell != null) { cell.gameObject.transform.SetParent(cacheRoot); cachedCellList.Add(cell); } } public void AddOnvalueChangedFunc(Action func) { onValueChanged = func; } public void AddOnInstantiateCellAction(Action action) { luaInstantiateCellAction = action; } public void AddRefreshAction(Action action) { luaRefreshAction = action; } public void AddSetSelectedAction(Action action) { luaSetSelectedAction = action; } public void SetTotalCount(int count) { totalCount = count; } public int GetTotalCount() { return totalCount; } public void SetSelected(int index) { RefreshRealShowIndex(); selectedIndex = index; int count = allCells.Count; for (int i = 0; i < count; i++) { var cell = allCells[i]; luaSetSelectedAction?.Invoke(cell.index == index, cell.objectIndex); } } public int GetSelectedIndex() { return selectedIndex; } public void AddCell() { RefreshRealShowIndex(); totalCount++; if (IsNotFull()) { float size = NewItemAtEnd(); while (size > 0 && IsNotFull()) { size = NewItemAtEnd(); } } } public virtual bool IsNotFull() { return false; } public void RemoveCell(int index, bool alignFlag = true) { if (totalCount >= index + 1) { RefreshRealShowIndex(); if (index == selectedIndex) { selectedIndex = 0; } else { selectedIndex = Math.Max(0, index > selectedIndex ? selectedIndex : selectedIndex - 1); } cellStartIndex = index + 1 < cellStartIndex ? cellStartIndex - 1 : cellStartIndex; if (cellEndIndex == totalCount) { totalCount--; //已经滑到底端 DeleteItemAtEnd(); RefreshAll(); } else { totalCount--; RefreshAll(); } } } public void ClearCells() { if (Application.isPlaying) { int count = cellEndIndex - cellStartIndex; cellStartIndex = 0; cellEndIndex = 0; curTopIndex = 0; curBottomIndex = 0; selectedIndex = 0; for (int i = count - 1; i >= 0; i--) { var gameObject = content.GetChild(i).gameObject; CellObj cell = GetCell(gameObject); if (cell != null) { CacheCell(cell); } } } } protected CellObj GetCell(GameObject gameObject) { int count = allCells.Count; for (int i = count - 1; i >= 0; i--) { if (allCells[i] != null && allCells[i].gameObject == gameObject) { return allCells[i]; } } return default; } public virtual void RefreshRealShowIndex() { curTopIndex = 0; curBottomIndex = 0; } public void ScrollToCellImmediately(int index, bool alignFlag = true) { RefreshRealShowIndex(); if (alignFlag) { RefillCells(index); } else { RefillCellsFromEnd(index); } } public void ScrollToCell(int index, float speed, bool alignFlag = true) { if (totalCount >= 0 && (index < 0 || index >= totalCount)) return; if (speed <= 0) return; StopAllCoroutines(); StartCoroutine(ScrollToCellCoroutine(index, speed, alignFlag)); } IEnumerator ScrollToCellCoroutine(int index, float speed, bool alignFlag = true) { bool needMoving = true; while (needMoving) { yield return null; if (!dragging) { float move = 0; if (index < cellStartIndex) move = -Time.deltaTime * speed; else if (index >= cellEndIndex) move = Time.deltaTime * speed; else { viewBounds = new Bounds(viewport.rect.center, viewport.rect.size); var m_ItemBounds = GetBounds4Item(index); var offset = 0.0f; if (directionSign == -1) if (alignFlag) { offset = viewBounds.max.y - m_ItemBounds.max.y; } else { offset = viewBounds.max.y - m_ItemBounds.min.y; } else if (directionSign == 1) if (alignFlag) { offset = m_ItemBounds.min.x - viewBounds.min.x; } else { offset = m_ItemBounds.min.x - viewBounds.max.x; } // check if we cannot move on if (totalCount >= 0) { if (offset > 0 && cellEndIndex == totalCount && !alignFlag) { m_ItemBounds = GetBounds4Item(totalCount - 1); // reach bottom if ((directionSign == -1 && m_ItemBounds.min.y > viewBounds.min.y) || (directionSign == 1 && m_ItemBounds.max.x < viewBounds.max.x)) { needMoving = false; break; } } else if (offset < 0 && cellStartIndex == 0 && alignFlag) { m_ItemBounds = GetBounds4Item(0); if ((directionSign == -1 && m_ItemBounds.max.y < viewBounds.max.y) || (directionSign == 1 && m_ItemBounds.min.x > viewBounds.min.x)) { needMoving = false; break; } } } float maxMove = Time.deltaTime * speed; if (Mathf.Abs(offset) < maxMove) { needMoving = false; move = offset; } else move = Mathf.Sign(offset) * maxMove; } if (Math.Abs(move) > 0) { Vector2 offset = GetVector(move); content.anchoredPosition += offset; prevPosition += offset; contentStartPosition += offset; } } } StopMovement(); UpdatePrevData(); } public void RefreshAll() { if (Application.isPlaying && this.isActiveAndEnabled) { for (var i = cellEndIndex - cellStartIndex - 1; i >= 0 ; i-- ) { var gameObject = content.GetChild(i).gameObject; CellObj cell = GetCell(gameObject); cell.index = cellStartIndex + i; luaRefreshAction?.Invoke(cell.index, cell.objectIndex); } } } public void RefillCellsFromEnd(int offset = 0) { StopMovement(); for (var i = cellEndIndex - cellStartIndex - 1; i >= 0 ; i-- ) { var gameObject = content.GetChild(i).gameObject; CacheCell(GetCell(gameObject)); } cellEndIndex = totalCount - offset; cellStartIndex = cellEndIndex; curTopIndex = 0; curBottomIndex = 0; if (totalCount >= 0 && ContentConstraintCount != 1) Debug.LogWarning("FromEnd ContentConstraintCount should be 1"); float sizeToFill = 0, sizeFilled = 0; if (directionSign == -1) sizeToFill = viewport.rect.size.y; else sizeToFill = viewport.rect.size.x; bool sizeFlag = true; while (sizeToFill > sizeFilled) { float size; if (sizeFlag) { size = NewItemAtStart(); if (size <= 0) sizeFlag = false; } else { size = NewItemAtEnd(); if (size <= 0) break; } sizeFilled += size; } //溢出 Vector2 pos = content.anchoredPosition; float dist = - Mathf.Max(0, sizeFilled - sizeToFill); if (directionSign == -1) pos.y = -dist; else if (directionSign == 1) pos.x = -dist; content.anchoredPosition = pos; } public void RefiilCellsFromEndForFrame(int intervalFrame, int offset = 0) { if (gameObject.active) { StopMovement(); for (var i = cellEndIndex - cellStartIndex - 1; i >= 0 ; i-- ) { var gameObject = content.GetChild(i).gameObject; CacheCell(GetCell(gameObject)); } cellEndIndex = totalCount - offset; cellStartIndex = cellEndIndex; curTopIndex = 0; curBottomIndex = 0; if (totalCount >= 0 && ContentConstraintCount != 1) Debug.LogWarning("RefillFromEnd ContentConstraintCount should be 1"); StopCoroutine("RefillCellsFromEndForFrameCoroutine"); StartCoroutine("RefillCellsFromEndForFrameCoroutine", intervalFrame); } } IEnumerator RefillCellsFromEndForFrameCoroutine(int intervaleFrame) { var waitForEndOfFatme = new WaitForEndOfFrame(); float sizeToFill = 0, sizeFilled = 0; if (directionSign == -1) sizeToFill = viewport.rect.size.y; else sizeToFill = viewport.rect.size.x; bool sizeFlag = true; while (sizeToFill > sizeFilled) { float size; if (sizeFlag) { size = NewItemAtStart(); if (size <= 0) sizeFlag = false; for (var i = 0; i < intervaleFrame; ++i) { yield return waitForEndOfFatme; } } else { size = NewItemAtEnd(); if (size <= 0) break; for (var i = 0; i < intervaleFrame; ++i) { yield return waitForEndOfFatme; } } sizeFilled += size; } //溢出 Vector2 pos = content.anchoredPosition; float dist = - Mathf.Max(0, sizeFilled - sizeToFill); if (directionSign == -1) pos.y = -dist; else if (directionSign == 1) pos.x = -dist; content.anchoredPosition = pos; } public void RefillCells(int index = 0) { StopMovement(); for (var i = cellEndIndex - cellStartIndex - 1; i >= 0 ; i-- ) { var gameObject = content.GetChild(i).gameObject; CacheCell(GetCell(gameObject)); } if (index > 0 && index > ContentConstraintCount) { var offset = index % ContentConstraintCount; if (offset == 0) { cellStartIndex = index - ContentConstraintCount; } else { cellStartIndex = index - offset; } } else { cellStartIndex = 0; } cellEndIndex = cellStartIndex; curTopIndex = 0; curBottomIndex = 0; // Don't `Canvas.ForceUpdateCanvases();` here, or it will new/delete cells to change itemTypeStart/End float sizeToFill = 0, sizeFilled = 0; // m_ViewBounds may be not ready when RefillCells on Start if (directionSign == -1) sizeToFill = viewport.rect.size.y; else sizeToFill = viewport.rect.size.x; bool sizeFlag = true; while (sizeToFill > sizeFilled) { float size; if (sizeFlag) { size = NewItemAtEnd(); if (size <= 0) sizeFlag = false; } else { size = NewItemAtStart(); if (size <= 0) break; } sizeFilled += size; } Vector2 pos = content.anchoredPosition; if (directionSign == -1) pos.y = 0; else if (directionSign == 1) pos.x = 0; content.anchoredPosition = pos; } public void RefillCellsForFrame(int intervalFrame, int index = 0) { if (gameObject.active) { StopMovement(); for (var i = cellEndIndex - cellStartIndex - 1; i >= 0 ; i-- ) { var gameObject = content.GetChild(i).gameObject; CacheCell(GetCell(gameObject)); } if (index > 0 && index > ContentConstraintCount) { var offset = index % ContentConstraintCount; if (offset == 0) { cellStartIndex = index - ContentConstraintCount; } else { cellStartIndex = index - offset; } } else { cellStartIndex = 0; } cellEndIndex = cellStartIndex; curTopIndex = 0; curBottomIndex = 0; // Don't `Canvas.ForceUpdateCanvases();` here, or it will new/delete cells to change itemTypeStart/End Vector2 pos = content.anchoredPosition; if (directionSign == -1) pos.y = 0; else if (directionSign == 1) pos.x = 0; content.anchoredPosition = pos; StopCoroutine("RefillCellsForFrameCoroutine"); StartCoroutine("RefillCellsForFrameCoroutine", intervalFrame); } } IEnumerator RefillCellsForFrameCoroutine(int intervaleFrame) { var waitForEndOfFatme = new WaitForEndOfFrame(); float sizeToFill = 0, sizeFilled = 0; // m_ViewBounds may be not ready when RefillCells on Start if (directionSign == -1) sizeToFill = viewport.rect.size.y; else sizeToFill = viewport.rect.size.x; var sizeFlag = true; while (sizeToFill > sizeFilled) { float size = 0; if (sizeFlag) { if (totalCount >= 0 && cellEndIndex >= totalCount) { size = 0; } else { int count = ContentConstraintCount - (cellEndIndex - cellStartIndex) % ContentConstraintCount; for (int i = 0; i < count; i++) { RectTransform newItem = InstantiateNextItem(cellEndIndex); if (newItem == null) { break; } size = GetSize(newItem); //size = Mathf.Max(GetSize(newItem), size); cellEndIndex++; if (totalCount >= 0 && cellEndIndex >= totalCount) { break; } for (var w = 0; w < intervaleFrame; ++w) { yield return waitForEndOfFatme; } } endThreshold = size * 1.5f; if (startThreshold < 0) { startThreshold = size * 1.5f; } } if (size <= 0) { sizeFlag = false; } } else { if (totalCount >= 0 && cellStartIndex - ContentConstraintCount < 0) { size = 0; } else { for (int i = 0; i < ContentConstraintCount; i++) { cellStartIndex--; RectTransform newItem = InstantiateNextItem(cellStartIndex); if (newItem == null) { size = 0; } newItem.SetAsFirstSibling(); size = GetSize(newItem); //size = Mathf.Max(GetSize(newItem), size); for (var w = 0; w < intervaleFrame; ++w) { yield return waitForEndOfFatme; } } startThreshold = size * 1.5f; if (endThreshold < 0) { endThreshold =size * 1.5f; } Vector2 offset = GetVector(size); content.anchoredPosition += offset; prevPosition += offset; contentStartPosition += offset; } if (size <= 0) { break; } } sizeFilled += size; } } protected float NewItemAtStart() { if (totalCount >= 0 && cellStartIndex - ContentConstraintCount < 0) return 0; float size = 0; for (int i = 0; i < ContentConstraintCount; i++) { cellStartIndex--; RectTransform newItem = InstantiateNextItem(cellStartIndex); if (newItem == null) return 0; newItem.SetAsFirstSibling(); size = GetSize(newItem); //size = Mathf.Max(GetSize(newItem), size); } startThreshold = size * 1.5f; if (endThreshold < 0) { endThreshold = size * 1.5f; } Vector2 offset = GetVector(size); content.anchoredPosition += offset; prevPosition += offset; contentStartPosition += offset; return size; } protected float DeleteItemAtStart() { // special case: when moving or dragging, we cannot simply delete start when we've reached the end if (((dragging || velocity != Vector2.zero) && totalCount >= 0 && cellEndIndex >= totalCount - 1) || (cellEndIndex == 0 && cellStartIndex == 0))//content.childCount == 0) return 0; float size = 0; for (int i = 0; i < ContentConstraintCount; i++) { RectTransform oldItem = content.GetChild(0) as RectTransform; size = Mathf.Max(GetSize(oldItem), size); CacheCell(GetCell(oldItem.gameObject)); cellStartIndex++; if ((cellEndIndex == 0 && cellStartIndex == 0))//content.childCount == 0) break; } Vector2 offset = GetVector(size); content.anchoredPosition -= offset; prevPosition -= offset; contentStartPosition -= offset; return size; } protected float NewItemAtEnd() { if (totalCount >= 0 && cellEndIndex >= totalCount) return 0; float size = 0; // issue 4: fill lines to end first //int count = ContentConstraintCount - (content.childCount % ContentConstraintCount); int count = ContentConstraintCount - (cellEndIndex - cellStartIndex) % ContentConstraintCount; for (int i = 0; i < count; i++) { RectTransform newItem = InstantiateNextItem(cellEndIndex); if (newItem == null) break; size = GetSize(newItem); //size = Mathf.Max(GetSize(newItem), size); cellEndIndex++; if (totalCount >= 0 && cellEndIndex >= totalCount) { break; } } endThreshold = size * 1.5f; if (startThreshold < 0) { startThreshold = size * 1.5f; } return size; } protected float DeleteItemAtEnd() { if (((dragging || velocity != Vector2.zero) && totalCount >= 0 && cellStartIndex < ContentConstraintCount) || (cellEndIndex == 0 && cellStartIndex == 0))//|| content.childCount == 0) return 0; float size = 0; for (int i = 0; i < ContentConstraintCount; i++) { var index = cellEndIndex - cellStartIndex - 1; if (index < 0 || index > content.childCount - 1) { Debug.LogWarning(string.Format("caijieLog try get child out of bounds index : {0}, childCount : {1}, totalCount : {2} ", index, content.childCount, totalCount)); break; } RectTransform oldItem = content.GetChild(index) as RectTransform; size = Mathf.Max(GetSize(oldItem), size); CacheCell(GetCell(oldItem.gameObject)); cellEndIndex--; if (cellEndIndex % ContentConstraintCount == 0 || (cellEndIndex == 0 && cellStartIndex == 0) || totalCount >= 0 && cellEndIndex >= totalCount) //content.childCount == 0) break; //just delete the whole row } return size; } RectTransform InstantiateNextItem(int index) { if (totalCount < index + 1) { return null; } RectTransform nextItemTrans = null; CellObj nextCell = null; if (cachedCellList.Count > 0) { int cachedCellIndex = cachedCellList.Count - 1; nextItemTrans = cachedCellList[cachedCellIndex].gameObject.transform as RectTransform; nextCell = cachedCellList[cachedCellIndex]; cachedCellList.RemoveAt(cachedCellIndex); nextItemTrans.transform.SetParent(content, false); nextItemTrans.gameObject.SetActive(true); }else{ nextItemTrans = Instantiate(bfCell.CachedRectTransform); nextItemTrans.SetParent(content, false); nextItemTrans.gameObject.SetActive(true); nextCell = new CellObj(); objectIndex++; nextCell.objectIndex = objectIndex; nextCell.gameObject = nextItemTrans.gameObject; luaInstantiateCellAction?.Invoke(nextCell.gameObject); } nextCell.index = index; allCells.Add(nextCell); luaRefreshAction?.Invoke(index, nextCell.objectIndex); return nextItemTrans; } public virtual void Rebuild(CanvasUpdate executing) { if (executing == CanvasUpdate.Prelayout) if (useBar) { UpdateCachedData(); } if (executing == CanvasUpdate.PostLayout) { UpdateBounds(); if (useBar) { UpdateScrollbars(Vector2.zero); } UpdatePrevData(); hasRebuiltLayout = true; } } public virtual void LayoutComplete() { } public virtual void GraphicUpdateComplete() { } void UpdateCachedData() { if (!useBar) { return; } horizontalScrollbarRect = horizontalScrollbar == null ? null : horizontalScrollbar.transform as RectTransform; verticalScrollbarRect = verticalScrollbar == null ? null : verticalScrollbar.transform as RectTransform; // These are true if either the elements are children, or they don't exist at all. bool viewIsChild = (viewport.parent == transform); bool hScrollbarIsChild = (!horizontalScrollbarRect || horizontalScrollbarRect.parent == transform); bool vScrollbarIsChild = (!verticalScrollbarRect || verticalScrollbarRect.parent == transform); bool allAreChildren = (viewIsChild && hScrollbarIsChild && vScrollbarIsChild); hSliderExpand = allAreChildren && horizontalScrollbarRect && HorizontalScrollbarVisibility == ScrollbarVisibility.AutoHideAndExpandViewport; vSliderExpand = allAreChildren && verticalScrollbarRect && VerticalScrollbarVisibility == ScrollbarVisibility.AutoHideAndExpandViewport; hSliderHeight = (horizontalScrollbarRect == null ? 0 : horizontalScrollbarRect.rect.height); vSliderWidth = (verticalScrollbarRect == null ? 0 : verticalScrollbarRect.rect.width); } public override bool IsActive() { return base.IsActive() && content != null; } void EnsureLayoutHasRebuilt() { if (!hasRebuiltLayout && !CanvasUpdateRegistry.IsRebuildingLayout()) Canvas.ForceUpdateCanvases(); } public virtual void StopMovement() { velocity = Vector2.zero; } public virtual void OnScroll(PointerEventData data) { if (!IsActive()) return; EnsureLayoutHasRebuilt(); UpdateBounds(); 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; } //2018new if (data.IsScrolling()) scrolling = true; Vector2 position = content.anchoredPosition; position += delta * scrollSensitivity; if (movementType == MovementType.Clamped) position += CalculateOffset(position - content.anchoredPosition); SetContentAnchoredPosition(position); UpdateBounds(); } public virtual void OnInitializePotentialDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; velocity = Vector2.zero; } public virtual void OnBeginDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; if (!IsActive()) return; UpdateBounds(); pointerStartLocalCursor = Vector2.zero; RectTransformUtility.ScreenPointToLocalPointInRectangle(viewport, eventData.position, eventData.pressEventCamera, out pointerStartLocalCursor); contentStartPosition = content.anchoredPosition; dragging = true; } public virtual void OnEndDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; dragging = false; } public virtual void OnDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; if (!IsActive()) return; Vector2 localCursor; if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(viewport, eventData.position, eventData.pressEventCamera, out localCursor)) return; UpdateBounds(); var pointerDelta = localCursor - pointerStartLocalCursor; Vector2 position = contentStartPosition + pointerDelta; // Offset to get content into place in the view. Vector2 offset = CalculateOffset(position - content.anchoredPosition); position += offset; if (movementType == MovementType.Elastic) { if (offset.x != 0) position.x = position.x - RubberDelta(offset.x, viewBounds.size.x) * rubberScale; if (offset.y != 0) position.y = position.y - RubberDelta(offset.y, viewBounds.size.y) * rubberScale; } SetContentAnchoredPosition(position); } protected virtual void SetContentAnchoredPosition(Vector2 position) { if (!horizontal) position.x = content.anchoredPosition.x; if (!vertical) position.y = content.anchoredPosition.y; if (position != content.anchoredPosition) { content.anchoredPosition = position; UpdateBounds(true); } } protected virtual void LateUpdate() { if (!content) return; EnsureLayoutHasRebuilt(); UpdateBounds(); float deltaTime = Time.unscaledDeltaTime; Vector2 offset = CalculateOffset(Vector2.zero); if (!dragging && (offset != Vector2.zero || velocity != Vector2.zero)) { Vector2 position = 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 (movementType == MovementType.Elastic && offset[axis] != 0) { float speed = velocity[axis]; //2018new var smoothTime = elasticity; // if (scrolling) // { // smoothTime *= 3.0f; // } position[axis] = Mathf.SmoothDamp(content.anchoredPosition[axis], content.anchoredPosition[axis] + offset[axis], ref speed, smoothTime, Mathf.Infinity, deltaTime); if (Mathf.Abs(speed) < 1) { speed = 0; } velocity[axis] = speed; } // Else move content according to velocity with deceleration applied. else if (inertia) { velocity[axis] *= Mathf.Pow(decelerationRate, deltaTime); if (Mathf.Abs(velocity[axis]) < 1) velocity[axis] = 0; position[axis] += velocity[axis] * deltaTime; } // If we have neither elaticity or friction, there shouldn't be any velocity. else velocity[axis] = 0; } if (velocity != Vector2.zero) { if (movementType == MovementType.Clamped) { offset = CalculateOffset(position - content.anchoredPosition); position += offset; } SetContentAnchoredPosition(position); } } if (dragging && inertia) { Vector3 newVelocity = (content.anchoredPosition - prevPosition) / deltaTime; velocity = Vector3.Lerp(velocity, newVelocity, deltaTime * 10); } if (viewBounds != prevViewBounds || contentBounds != prevContentBounds || content.anchoredPosition != prevPosition) { if (useBar) { UpdateScrollbars(offset); } onValueChanged?.Invoke(NormalizedPosition); UpdatePrevData(); } //2018new if (useBar) { UpdateScrollbarVisibility(); } scrolling = false; hasRebuiltLayout = false; } void UpdatePrevData() { if (content == null) prevPosition = Vector2.zero; else prevPosition = content.anchoredPosition; prevViewBounds = viewBounds; prevContentBounds = contentBounds; } void UpdateScrollbars(Vector2 offset) { if (horizontalScrollbar) { if (contentBounds.size.x > 0 && totalCount > 0) horizontalScrollbar.size = Mathf.Clamp01((viewBounds.size.x - Mathf.Abs(offset.x)) / contentBounds.size.x * (cellEndIndex - cellStartIndex) / totalCount); else horizontalScrollbar.size = 1; horizontalScrollbar.value = HorizontalNormalizedPosition; } if (verticalScrollbar) { if (contentBounds.size.y > 0 && totalCount > 0) verticalScrollbar.size = Mathf.Clamp01((viewBounds.size.y - Mathf.Abs(offset.y)) / contentBounds.size.y * (cellEndIndex - cellStartIndex) / totalCount); else verticalScrollbar.size = 1; verticalScrollbar.value = VerticalNormalizedPosition; } } public Vector2 NormalizedPosition { get { return new Vector2(HorizontalNormalizedPosition, VerticalNormalizedPosition); } set { SetNormalizedPosition(value.x, 0); SetNormalizedPosition(value.y, 1); } } public float HorizontalNormalizedPosition { get { UpdateBounds(); if (totalCount > 0 && cellEndIndex > cellStartIndex) { //TODO: consider contentSpacing float elementSize = contentBounds.size.x / (cellEndIndex - cellStartIndex); float totalSize = elementSize * totalCount; float offset = contentBounds.min.x - elementSize * cellStartIndex; if (totalSize <= viewBounds.size.x) return (viewBounds.min.x > offset) ? 1 : 0; return (viewBounds.min.x - offset) / (totalSize - viewBounds.size.x); } else return 0.5f; } set { SetNormalizedPosition(value, 0); } } public float VerticalNormalizedPosition { get { UpdateBounds(); if (totalCount > 0 && cellEndIndex > cellStartIndex) { //TODO: consider contentSpacinge float elementSize = contentBounds.size.y / (cellEndIndex - cellStartIndex); float totalSize = elementSize * totalCount; float offset = contentBounds.max.y + elementSize * cellStartIndex; if (totalSize <= viewBounds.size.y) return (offset > viewBounds.max.y) ? 1 : 0; return (offset - viewBounds.max.y) / (totalSize - viewBounds.size.y); } else return 0.5f; } set { SetNormalizedPosition(value, 1); } } void SetHorizontalNormalizedPosition(float value) { SetNormalizedPosition(value, 0); } void SetVerticalNormalizedPosition(float value) { SetNormalizedPosition(value, 1); } void SetNormalizedPosition(float value, int axis) { if (totalCount <= 0 || cellEndIndex <= cellStartIndex) return; EnsureLayoutHasRebuilt(); UpdateBounds(); Vector3 localPosition = content.localPosition; float newLocalPosition = localPosition[axis]; if (axis == 0) { float elementSize = contentBounds.size.x / (cellEndIndex - cellStartIndex); float totalSize = elementSize * totalCount; float offset = contentBounds.min.x - elementSize * cellStartIndex; newLocalPosition += viewBounds.min.x - value * (totalSize - viewBounds.size[axis]) - offset; } else if (axis == 1) { float elementSize = contentBounds.size.y / (cellEndIndex - cellStartIndex); float totalSize = elementSize * totalCount; float offset = contentBounds.max.y + elementSize * cellStartIndex; newLocalPosition -= offset - value * (totalSize - viewBounds.size.y) - viewBounds.max.y; } if (Mathf.Abs(localPosition[axis] - newLocalPosition) > 0.01f) { localPosition[axis] = newLocalPosition; content.localPosition = localPosition; velocity[axis] = 0; UpdateBounds(true); } } static float RubberDelta(float overStretching, float viewSize) { return (1 - (1 / ((Mathf.Abs(overStretching) * 0.55f / viewSize) + 1))) * viewSize * Mathf.Sign(overStretching); } protected override void OnRectTransformDimensionsChange() { SetDirty(); } bool hScrollingNeeded { get { if (Application.isPlaying) return contentBounds.size.x > viewBounds.size.x + 0.01f; return true; } } bool vScrollingNeeded { get { if (Application.isPlaying) return contentBounds.size.y > viewBounds.size.y + 0.01f; return true; } } public virtual void CalculateLayoutInputHorizontal() { } public virtual void CalculateLayoutInputVertical() { } public virtual float minWidth { get { return -1; } } public virtual float preferredWidth { get { return -1; } } public virtual float flexibleWidth { get; private set; } public virtual float minHeight { get { return -1; } } public virtual float preferredHeight { get { return -1; } } public virtual float flexibleHeight { get { return -1; } } public virtual int layoutPriority { get { return -1; } } public virtual void SetLayoutHorizontal() { tracker.Clear(); if (hSliderExpand || vSliderExpand) { tracker.Add(this, viewport, DrivenTransformProperties.Anchors | DrivenTransformProperties.SizeDelta | DrivenTransformProperties.AnchoredPosition); // Make view full size to see if content fits. viewport.anchorMin = Vector2.zero; viewport.anchorMax = Vector2.one; viewport.sizeDelta = Vector2.zero; viewport.anchoredPosition = Vector2.zero; // Recalculate content layout with this size to see if it fits when there are no scrollbars. LayoutRebuilder.ForceRebuildLayoutImmediate(content); viewBounds = new Bounds(viewport.rect.center, viewport.rect.size); contentBounds = GetBounds(); } // If it doesn't fit vertically, enable vertical scrollbar and shrink view horizontally to make room for it. if (vSliderExpand && vScrollingNeeded) { viewport.sizeDelta = new Vector2(-(vSliderWidth + verticalScrollbarSpacing), viewport.sizeDelta.y); // Recalculate content layout with this size to see if it fits vertically // when there is a vertical scrollbar (which may reflowed the content to make it taller). LayoutRebuilder.ForceRebuildLayoutImmediate(content); viewBounds = new Bounds(viewport.rect.center, viewport.rect.size); contentBounds = GetBounds(); } // If it doesn't fit horizontally, enable horizontal scrollbar and shrink view vertically to make room for it. if (hSliderExpand && hScrollingNeeded) { viewport.sizeDelta = new Vector2(viewport.sizeDelta.x, -(hSliderHeight + horizontalScrollbarSpacing)); viewBounds = new Bounds(viewport.rect.center, viewport.rect.size); contentBounds = GetBounds(); } // If the vertical slider didn't kick in the first time, and the horizontal one did, // we need to check again if the vertical slider now needs to kick in. // If it doesn't fit vertically, enable vertical scrollbar and shrink view horizontally to make room for it. if (vSliderExpand && vScrollingNeeded && viewport.sizeDelta.x == 0 && viewport.sizeDelta.y < 0) viewport.sizeDelta = new Vector2(-(vSliderWidth + verticalScrollbarSpacing), viewport.sizeDelta.y); } public virtual void SetLayoutVertical() { if (useBar) { UpdateScrollbarLayout(); } viewBounds = new Bounds(viewport.rect.center, viewport.rect.size); contentBounds = GetBounds(); } void UpdateScrollbarVisibility() { if (verticalScrollbar && verticalScrollbarVisibility != ScrollbarVisibility.Permanent && verticalScrollbar.gameObject.activeSelf != vScrollingNeeded) verticalScrollbar.gameObject.SetActive(vScrollingNeeded); if (horizontalScrollbar && horizontalScrollbarVisibility != ScrollbarVisibility.Permanent && horizontalScrollbar.gameObject.activeSelf != hScrollingNeeded) horizontalScrollbar.gameObject.SetActive(hScrollingNeeded); } void UpdateScrollbarLayout() { if (!useBar) { return; } if (vSliderExpand && horizontalScrollbar) { tracker.Add(this, horizontalScrollbarRect, DrivenTransformProperties.AnchorMinX | DrivenTransformProperties.AnchorMaxX | DrivenTransformProperties.SizeDeltaX | DrivenTransformProperties.AnchoredPositionX); horizontalScrollbarRect.anchorMin = new Vector2(0, horizontalScrollbarRect.anchorMin.y); horizontalScrollbarRect.anchorMax = new Vector2(1, horizontalScrollbarRect.anchorMax.y); horizontalScrollbarRect.anchoredPosition = new Vector2(0, horizontalScrollbarRect.anchoredPosition.y); if (vScrollingNeeded) horizontalScrollbarRect.sizeDelta = new Vector2(-(vSliderWidth + verticalScrollbarSpacing), horizontalScrollbarRect.sizeDelta.y); else horizontalScrollbarRect.sizeDelta = new Vector2(0, horizontalScrollbarRect.sizeDelta.y); } if (hSliderExpand && verticalScrollbar) { tracker.Add(this, verticalScrollbarRect, DrivenTransformProperties.AnchorMinY | DrivenTransformProperties.AnchorMaxY | DrivenTransformProperties.SizeDeltaY | DrivenTransformProperties.AnchoredPositionY); verticalScrollbarRect.anchorMin = new Vector2(verticalScrollbarRect.anchorMin.x, 0); verticalScrollbarRect.anchorMax = new Vector2(verticalScrollbarRect.anchorMax.x, 1); verticalScrollbarRect.anchoredPosition = new Vector2(verticalScrollbarRect.anchoredPosition.x, 0); if (hScrollingNeeded) verticalScrollbarRect.sizeDelta = new Vector2(verticalScrollbarRect.sizeDelta.x, -(hSliderHeight + horizontalScrollbarSpacing)); else verticalScrollbarRect.sizeDelta = new Vector2(verticalScrollbarRect.sizeDelta.x, 0); } } void UpdateBounds(bool updateItems = false) { viewBounds = new Bounds(viewport.rect.center, viewport.rect.size); contentBounds = GetBounds(); if (content == null) return; // Don't do this in Rebuild if (Application.isPlaying && updateItems && UpdateItems(viewBounds, contentBounds)) { Canvas.ForceUpdateCanvases(); contentBounds = GetBounds(); } // Make sure content bounds arAdjustBoundse at least 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 = contentBounds.size; Vector3 contentPos = contentBounds.center; Vector3 excess = viewBounds.size - contentSize; if (excess.x > 0) { contentPos.x -= excess.x * (content.pivot.x - 0.5f); contentSize.x = viewBounds.size.x; } if (excess.y > 0) { contentPos.y -= excess.y * (content.pivot.y - 0.5f); contentSize.y = viewBounds.size.y; } contentBounds.size = contentSize; contentBounds.center = contentPos; } protected virtual bool UpdateItems(Bounds viewBounds, Bounds contentBounds) { return false; } readonly Vector3[] m_Corners = new Vector3[4]; Bounds GetBounds() { if (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 = viewport.worldToLocalMatrix; 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; } Bounds GetBounds4Item(int index) { if (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 = viewport.worldToLocalMatrix; int offset = index - cellStartIndex; if (offset < 0 || offset >= cellEndIndex - cellStartIndex)//content.childCount) return new Bounds(); RectTransform rectTransfrom = content.GetChild(offset) as RectTransform; if (rectTransfrom == null) return new Bounds(); rectTransfrom.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; } protected Bounds GetCellBounds(int index) { if (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 = viewport.worldToLocalMatrix; var rt = content.GetChild(index) as RectTransform; if (rt == null) return new Bounds(); rt.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; } Vector2 CalculateOffset(Vector2 delta) { Vector2 offset = Vector2.zero; if (movementType == MovementType.Unrestricted) return offset; Vector2 min = contentBounds.min; Vector2 max = contentBounds.max; if (horizontal) { min.x += delta.x; max.x += delta.x; if (min.x > viewBounds.min.x) offset.x = viewBounds.min.x - min.x; else if (max.x < viewBounds.max.x) offset.x = viewBounds.max.x - max.x; } if (vertical) { min.y += delta.y; max.y += delta.y; if (max.y < viewBounds.max.y) offset.y = viewBounds.max.y - max.y; else if (min.y > viewBounds.min.y) offset.y = viewBounds.min.y - min.y; } return offset; } protected void SetDirty() { if (!IsActive()) return; LayoutRebuilder.MarkLayoutForRebuild(RectTrans); } protected void SetDirtyCaching() { if (!IsActive()) return; CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this); LayoutRebuilder.MarkLayoutForRebuild(RectTrans); } #if UNITY_EDITOR protected override void OnValidate() { SetDirtyCaching(); } #endif } }