主要思路 用几个预先实例化的子item来轮流显示内容,子item前后都可以先进先出,这个过程可以用一个双向队列来实现。
主要用到的UGUI组件是ScrollRect
,其中viewport
属性对应可以看见的视口,content
属性对应内容窗口,内容窗口的大小决定了滚动条可以拉动的范围。
虽然实际上只有几个实例化的item,但是为了能够拉动滑动条,还是需要先调整content
的高度为所有数据的总高度
1 2 3 4 5 6 private void UpdateContentHeight ( ) { var sizeDelta = scrollRect.content.sizeDelta; sizeDelta.y = itemOrigin.rect.height * dataRowCount + rowPadding * (dataRowCount - 1 ); scrollRect.content.sizeDelta = sizeDelta; }
更新item时首先需要判断item的数据索引是否有效,无效的话就SetActive(false)
。因为几个复用的item都有可能马上被显示,所以实际上只有当拉到最上面或者最下面的时候才会有item被取消激活。
1 2 3 4 5 6 var itemTransform = item.RectTransform;if (dataIndex < 0 || dataIndex >= dataRowCount){ itemTransform.gameObject.SetActive(false ); return ; }
之后设置item的坐标,对应的是anchoredPosition
属性。首先计算处第一个item当前的坐标,然后按照从上到下的次序减去行高和行间距就得到了对应item的坐标
1 2 3 4 5 6 7 private void UpdateItemPosition (RectTransform item, int itemIndex ) { Vector2 anchoredPos; anchoredPos.y = _firstItemY - (_rowHeight * itemIndex + (1 - _itemPivot.y) * _itemRect.height); anchoredPos.x = _itemPivot.x * _itemRect.width; item.anchoredPosition = anchoredPos; }
对于更新item数据,这里声明了一个ILoopScrollItem
接口,其中就有UpdateData
方法,参数是数据的绝对索引,可以用来指定item需要显示的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface ILoopScrollViewItem { void UpdateData (int dataIndex ) ; RectTransform RectTransform { get ; } }
RectTransform.rect
属性在Awake
方法甚至Start
方法里可能获取到的值都是0(尤其是当设置ScrollView和Viewport的anchors为完全伸展的时候),于是代码里在Init
中加了额外判断,而且在Update
中进行重新初始化,具体可以看后面的代码
Deque Deque
是一个双向队列,底层数据结构是一个数组,这里是用来高效实现子item的动态进出。如果使用LinkedList
,其底层是双向链表,内存占用多一些,遍历开销大,不划算。
Deque
提供了MoveToFirst
和MoveToLast
方便快速移动大量首尾元素,而不用一个一个增删,尤其是当队列容量和元素个数相同,也就是满循环队列的时候会更加高效。
Deque
还实现了IEnumerable<T>
接口,可以支持foreach
遍历元素。具体可以查看后面的代码。
这个实现比较简单,没有处理异常,甚至没有考虑扩容,但是在这里是基本够用的
完整代码 具体实现可以参考完整的代码,注释很多。
这里还提供了一个简单的示例:新建一个Canvas
,在Content
下添加一个Panel
,再添加一个Text
,然后挂上脚本。结构如下:
1 2 3 4 5 ScrollView // LoopScrollView.cs, Test.cs Viewport Content Panel // TestItem.cs Text
LoopScrollView.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 using System;using UnityEngine;using UnityEngine.UI;namespace LoopScrollView { public class LoopScrollView : MonoBehaviour { [Tooltip("子节点模板" ) ] public RectTransform itemOriginTransform; [Tooltip("行间距" ) ] public float rowPadding = 15 ; public event Action<ILoopScrollViewItem> onAddItem; private Deque<ILoopScrollViewItem> items; public Deque<ILoopScrollViewItem> Items => items; private ScrollRect scrollRect; private RectTransform rectTransform; private const int resveredRows = 2 ; private int dataRowCount; public int DataRowCount { get => dataRowCount; set { if (dataRowCount != value ) { dataRowCount = value ; UpdateContentHeight(); UpdateAllItems(); } } } private float RowHeight => rowPadding + itemOriginTransform.rect.height; private float ViewportHeight => scrollRect.viewport.rect.height; private int FirstDataIndex => (int )(UpwardScrollHeight / RowHeight); private float UpwardScrollHeight => scrollRect.content.localPosition.y; private bool _hasInited, _initSucc; private void Update ( ) { if (_hasInited && !_initSucc) { Debug.LogWarning("LoopScrollView ReInit" ); Init(dataRowCount, onAddItem); if (_initSucc){ UpdateContentHeight(); UpdateAllItems(); } } } private void OnDisable ( ) { scrollRect.onValueChanged.RemoveListener(OnScroll); } public void Init (int dataRowCount, Action<ILoopScrollViewItem> onAddItem ) { _hasInited = true ; _initSucc = false ; this .onAddItem = onAddItem; rectTransform = GetComponent<RectTransform>(); scrollRect = GetComponent<ScrollRect>(); if (scrollRect == null ) { Debug.LogError("LoopScrollView need ScrollRect component!" ); return ; } if (itemOriginTransform == null ) { Debug.LogError("LoopScrollView need at leaest one child item!" ); return ; } scrollRect.onValueChanged.AddListener(OnScroll); itemOriginTransform.gameObject.SetActive(false ); if (ViewportHeight == 0 ) { Debug.LogWarning("LoopScrollView Init failed: ViewportHeight == 0" ); this .dataRowCount = dataRowCount; return ; } Debug.Log("ViewportHeight: " + ViewportHeight); int itemsCount = 2 * resveredRows + (int )(ViewportHeight / RowHeight + 0.5f ); items = new Deque<ILoopScrollViewItem>(itemsCount); items.AddLast(itemOriginTransform.GetComponent<ILoopScrollViewItem>()); onAddItem(items.First); for (int i = 1 ; i < itemsCount; i++) { items.AddLast(Instantiate(itemOriginTransform, scrollRect.content, false ).GetComponent<ILoopScrollViewItem>()); onAddItem(items.Last); } DataRowCount = dataRowCount; _initSucc = true ; Debug.Log("LoopScrollView Reinit Succ" ); return ; } private float _lastPosY = 0 ; private void OnScroll (Vector2 normalizedPos ) { float newPosY = scrollRect.content.localPosition.y; float deltaPosY = Mathf.Abs(newPosY - _lastPosY); if (deltaPosY >= ViewportHeight) { UpdateAllItems(); _lastPosY = newPosY; } else if (deltaPosY >= RowHeight) { int deltaRow = (int )(deltaPosY / RowHeight); if (newPosY > _lastPosY) { items.MoveToLast(deltaRow); } else { items.MoveToFirst(deltaRow); } UpdateAllItems(); _lastPosY = newPosY; } } public void UpdateAllItems ( ) { Debug.Log("LoopScrollView UpdateAllItems" ); PrepareForUpdateItems(); int itemIndex = -resveredRows, dataIndex = FirstDataIndex - resveredRows; foreach (var item in items) { UpdateItem(item, itemIndex++, dataIndex++); } } Vector2 _itemPivot; Rect _itemRect; float _rowHeight; float _firstItemY; private void PrepareForUpdateItems ( ) { _itemPivot = itemOriginTransform.pivot; _itemRect = itemOriginTransform.rect; _rowHeight = RowHeight; _firstItemY = - FirstDataIndex * _rowHeight; } private void UpdateItem (ILoopScrollViewItem item, int itemIndex, int dataIndex ) { var itemTransform = item.RectTransform; if (dataIndex < 0 || dataIndex >= dataRowCount) { itemTransform.gameObject.SetActive(false ); return ; } UpdateItemPosition(itemTransform, itemIndex); item.UpdateData(dataIndex); itemTransform.gameObject.SetActive(true ); } private void UpdateItemPosition (RectTransform item, int itemIndex ) { Vector2 anchoredPos; anchoredPos.y = _firstItemY - (_rowHeight * itemIndex + (1 - _itemPivot.y) * _itemRect.height); anchoredPos.x = _itemPivot.x * _itemRect.width; item.anchoredPosition = anchoredPos; } private void UpdateContentHeight ( ) { var sizeDelta = scrollRect.content.sizeDelta; sizeDelta.y = itemOriginTransform.rect.height * dataRowCount + rowPadding * (dataRowCount - 1 ); scrollRect.content.sizeDelta = sizeDelta; } } }
ILoopScrollViewItem.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using UnityEngine;namespace LoopScrollView { public interface ILoopScrollViewItem { void UpdateData (int dataIndex ) ; RectTransform RectTransform { get ; } } }
Deque.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 using System.Collections;using System.Collections.Generic;namespace LoopScrollView { public class Deque <T > : IEnumerable <T > { public Deque (int capcity ) { data = new T[capcity <= 0 ? 1 : capcity]; first = 0 ; last = 0 ; count = 0 ; } public void AddFirst (T value ) { if (count < data.Length) { if (first == 0 ) { first = data.Length - 1 ; data[first] = value ; } else { data[--first] = value ; } count++; } } public void AddLast (T value ) { if (Count < data.Length) { if (last == data.Length - 1 ) { data[last] = value ; last = 0 ; } else { data[last++] = value ; } count++; } } public T RemoveFirst ( ) { T ret = default (T); if (Count > 0 ) { ret = data[first]; data[first] = default (T); first = first == data.Length - 1 ? 0 : first + 1 ; count--; } return ret; } public T RemoveLast ( ) { T ret = default (T); if (Count > 0 ) { if (last == 0 ) { last = data.Length - 1 ; ret = data[last]; data[last] = default (T); } else { ret = data[--last]; data[last] = default (T); } count--; } return ret; } public void MoveToFirst (int n ) { if (n <= 0 || n >= count){ return ; } if (count == data.Length) { first = last - n; if (first < 0 ){ first += count; } last = first; } else { var tmp = data.Clone() as T[]; int index1 = first, index2 = (first + n) % count; do { data[index1++] = tmp[index2++]; index1 %= count; index2 %= count; }while (index1 != last); } } public void MoveToLast (int n ) { MoveToFirst(count - n); } public IEnumerator<T> GetEnumerator ( ) { return new Enumerator(this ); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(this ); } public int Count => count; public T First => data[first]; public T Last => last == 0 ? data[data.Length - 1 ] : data[last - 1 ]; public T this [int i] => data[(i + first) % data.Length]; private T[] data; private int first; private int last; private int count; public struct Enumerator : IEnumerator<T> { private Deque<T> deque; private int index; private bool started; public Enumerator (Deque<T> deque ) { this .deque = deque; index = deque.first - 1 ; started = false ; } public T Current => deque.data[index]; object IEnumerator.Current => deque.data[index]; public void Dispose ( ) {} public bool MoveNext ( ) { index = index == deque.data.Length - 1 ? 0 : index + 1 ; bool ret; if (deque.last > deque.first) { ret = index >= deque.first && index < deque.last; } else if (deque.last < deque.first) { ret = !(index >= deque.last && index < deque.first); } else { ret = !(started && index == deque.first); } if (!started) { started = true ; } return ret; } public void Reset ( ) { index = deque.first - 1 ; started = false ; } } } }
Test.cs
1 2 3 4 5 6 7 8 9 10 11 12 using UnityEngine;namespace LoopScrollView { public class Test : MonoBehaviour { void Start ( ) { GetComponent<LoopScrollView>().Init(1000 , (_) => { }); } } }
TestItem.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using UnityEngine;using UnityEngine.UI;namespace LoopScrollView { public class TestItem : MonoBehaviour , ILoopScrollViewItem { private RectTransform _rectTransform; public RectTransform RectTransform => _rectTransform ??= GetComponent<RectTransform>(); private Text text; public void UpdateData (int dataIndex ) { (text ??= GetComponentInChildren<Text>()).text = dataIndex.ToString(); } } }