Unity的ScrollView滚动视图复用

发现问题

在游戏开发中有一个常见的需求,就是需要在屏幕显示多个(多达上百)显示item,然后用户用手指滚动视图可以选择需要查看的item。

现在的情况是在100个data的时候,Unity引擎是直接创建出对应的100个显示item。

这样的问题是显示屏只有6~7个是当前用户看得到的,其余的90多个一直放在内存中,这样的处理是一个比较浪费内存空间的处理方法。

所以我们现在需要一种优化,就是在data有100个的时候,我们只创建显示区域的几个显示item就好了,然后这几个显示item,我们会复用起来,不断的更新data到这几个显示item上。

要完成以上逻辑,需要处理的地方有一下几个:

1.item的更新data回调

2.item的数量回调

3.计算item的index、尺寸及对应的位置

模仿FairyGUI的处理

在FairyGUI,对于前两个问题,FairyGUI中有“列表”组件来完成;对于第三个问题,就使用了虚拟列表,来完成这种优化,现在,我们来模仿FiryGUI的逻辑在Unity的组件中完成这个功能。

解决前两个问题

框架代码

首先,对于前两个问题,我们来做一个简单的自定义滚动视图(先不处理复用的逻辑)。

using System;
using UnityEngine;
using UnityEngine.UI;[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
public class ScrollView : ScrollRect
{[Tooltip("item的模板")]public RectTransform itemTemplate;//更新数据回调public Action<int, RectTransform> updateFunc;//设置数量回调(更新数据)public Func<int> itemCountFunc;public virtual void SetUpdateFunc(Action<int,RectTransform> func){updateFunc = func;}public virtual void SetItemCountFunc(Func<int> func){itemCountFunc = func;InternalUpdateData();}protected virtual void InternalUpdateData(){if (updateFunc == null){return;}RemoveAllChildren();for (int i = 0; i < itemCountFunc(); i++){GameObject itemObj = Instantiate(itemTemplate.gameObject, content, true);itemObj.transform.localPosition = itemTemplate.localPosition;itemObj.SetActive(true);updateFunc(i, itemObj.GetComponent<RectTransform>());}}public void RemoveAllChildren(){for(int i = 0;i < content.childCount; i++){Transform child = content.GetChild(i);if (itemTemplate != child){Destroy(child.gameObject);}}}
}

在这个脚本中,我们继承了ScrollRect组件,添加了item的更新数据回调;以及item的数据设置回调。

这两个问题的处理相对还算比较简单。

主要是通过回调来自定义data在对应显示item的创建。

脚本的在编辑器上显示为:

由于我们没有在ScrollView脚本中处理复用的逻辑,所以需要在显示对象Content上,添加Layout组件。

至此,我们解决前两个问题的框架的逻辑就处理好了。

示例

现在,我们贴出如何使用ScrollView的示例代码。

UIBoxRoguelike.cs

using UnityEngine.UI;/// <summary>
/// 宝箱翻牌UI
/// </summary>
public class UIBoxRoguelike : BasePanel
{public const string ItemsList = "ItemsList";// 奖励列表public const string ClaimMagicBox = "ClaimMagicBox";// 领取神秘宝箱/// <summary>/// 随机类型 0正常 1随机 2神秘/// </summary>public enum RoguelikeType{Normal = 0,Random,Secret,}public Image imgBoxIcon;public Button btnMask;public ScrollView roguelikeSr;private bool _isSecret;// 是否神秘奖励private void Start(){var type = RoguelikeType.Normal;var boxCfg = ConfigManager._BoxCfgMgr.GetDataByID((int)BoxModel.Box.BoxID);imgBoxIcon.sprite = AssetBundleMgr.GetInstance().LoadUISprite(boxCfg.Icon);// 根据当前宝箱btnMask.onClick.AddListener(() =>{for (int i = 0; i < BoxModel.ItemsList.Count; i++){// 存在神秘奖励 且 未领取if (BoxModel.ItemsList[i].Type != RoguelikeType.Secret && !BoxModel.HasSecretGet) continue;type = RoguelikeType.Secret;break;}// 存在神秘奖励 且 未领取if (type == RoguelikeType.Secret && !BoxModel.HasSecretGet){UIMgr.GetInstance().ShowPanel<UIBoxPop>(UIDef.UI_BOXPOP, BoxModel.Box);}else{UIMgr.GetInstance().ShowPanel<UIRewardPanel>(UIDef.UI_REWARDPANEL, BoxModel.RewardList.ToArray());TimerHelper.SetTimeOut(0.3f, () =>{UIMgr.GetInstance().ShowPanel<UIBoxDetail>(UIDef.UI_BOXDETAIL);});HideMe();}UIMgr.GetInstance().HidePanel(UIDef.UI_BOXDETAIL);});}public override void Notify(string msgType, object msgData){base.Notify(msgType, msgData);switch (msgType){case ItemsList:RefreshContent(msgData as RoguelikeItemData[]);break;case ClaimMagicBox:RefreshContent(msgData as RoguelikeItemData[]);break;}}private void RefreshContent(RoguelikeItemData[] data){roguelikeSr.SetUpdateFunc((index, rectTransform) =>{UIBoxRoguelikeItem item = rectTransform.GetComponent<UIBoxRoguelikeItem>();item.OnRefresh(data[index]);});roguelikeSr.SetItemCountFunc(() => data.Length);}
}

这个示例代码,我们主要看RefreshConent方法就好了。

另一个脚本,UIBoxRoguelikeItem.cs。

using System.Text;
using UnityEngine;
using UnityEngine.UI;public class UIBoxRoguelikeItem : MonoBehaviour
{public Image imgBg;public Image imgIcon;public Text txtTitle;public Text txtCount;public Button btnSecret;private RoguelikeItemData _data;private void Start(){btnSecret.onClick.AddListener(() =>{// 切换宝箱随机类型_data.Type = UIBoxRoguelike.RoguelikeType.Normal;// 刷新当前奖励信息OnRefresh(_data);// 禁用按钮btnSecret.gameObject.SetActive(false);});}public void OnRefresh(RoguelikeItemData data){_data = data;imgIcon.sprite = AssetBundleMgr.GetInstance().LoadUISprite(data.Icon);imgBg.sprite = AssetBundleMgr.GetInstance().LoadUISprite(GetIconBgPathByType(data.Type));txtTitle.text = data.Name;txtCount.text = data.Count.ToString();txtTitle.gameObject.SetActive(data.Type != UIBoxRoguelike.RoguelikeType.Secret);txtCount.gameObject.SetActive(data.Type != UIBoxRoguelike.RoguelikeType.Secret);imgIcon.gameObject.SetActive(data.Type != UIBoxRoguelike.RoguelikeType.Secret);btnSecret.gameObject.SetActive(data.Type == UIBoxRoguelike.RoguelikeType.Secret);}private string GetIconBgPathByType(UIBoxRoguelike.RoguelikeType type){StringBuilder iconBuilder = new StringBuilder();switch (type){case UIBoxRoguelike.RoguelikeType.Normal:iconBuilder.Append("UIAtlas/Box/card02_icon");break;case UIBoxRoguelike.RoguelikeType.Random:iconBuilder.Append("UIAtlas/Box/card01_icon");break;case UIBoxRoguelike.RoguelikeType.Secret:iconBuilder.Append("UIAtlas/Box/card03_icon");break;}return iconBuilder.ToString();}
}public class RoguelikeItemData
{public int ItemId;// 道具idpublic string Icon;// 图标public string Name;// 名字public int Count;// 数量public UIBoxRoguelike.RoguelikeType Type;// 随机类型public RoguelikeItemData(int itemId, string icon, string name, int count,UIBoxRoguelike.RoguelikeType type = UIBoxRoguelike.RoguelikeType.Normal){ItemId = itemId;Icon = icon;Name = name;Count = count;Type = type; // 是否神秘宝箱}
}

复用的逻辑处理

框架代码

好了,现在我们来处理第三个问题,第三个问题比前两个问题要复杂得多。

处理的主要两个脚本文件是ScrollViewEx.cs和ScollViewExItem.cs

ScrollViewEx.cs代码:

using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEngine.Events;[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
public class ScrollViewEx : ScrollView
{[SerializeField]private int m_pageSize = 50;public int pageSize => m_pageSize;private int startOffset = 0;private Func<int> realItemCountFunc;private bool canNextPage = false;public class ScrollItemWithRect{// scroll item 身上的 RectTransform组件public RectTransform item;// scroll item 在scrollview中的位置public Rect rect;// rect 是否需要更新public bool rectDirty = true;}int m_dataCount = 0;List<ScrollItemWithRect> managedItems = new List<ScrollItemWithRect>();// for hide and showpublic enum ItemLayoutType{// 最后一位表示滚动方向Vertical = 1,                   // 0001Horizontal = 2,                 // 0010VerticalThenHorizontal = 4,     // 0100HorizontalThenVertical = 5,     // 0101}public const int flagScrollDirection = 1;  // 0001[SerializeField]ItemLayoutType m_layoutType = ItemLayoutType.Vertical;protected ItemLayoutType layoutType { get { return m_layoutType; } }// const int 代替 enum 减少 (int)和(CriticalItemType)转换protected static class CriticalItemType{public const int UpToHide = 0;public const int DownToHide = 1;public const int UpToShow = 2;public const int DownToShow = 3;}// 只保存4个临界indexprotected int[] criticalItemIndex = new int[4];Rect refRect;// resource managementSimpleObjPool<RectTransform> itemPool = null;[Tooltip("初始化时池内item数量")]public int poolSize;[Tooltip("默认item尺寸")]public Vector2 defaultItemSize;[Tooltip("默认item间隔")]public Vector2 defaultItemSpace;//设置尺寸回调public Func<int, Vector2> itemSizeFunc;public Func<int, RectTransform> itemGetFunc;public Action<RectTransform> itemRecycleFunc;public Action<RectTransform> RecycleFunc;private Action UpdateCriticalItemsPreprocess = null;//选择元素回调private Action<int, RectTransform> selectIndexFunc;private UnityEvent<int, ScrollViewExItem> _onClickItem;// statusprivate bool initialized = false;private int willUpdateData = 0;public override void SetUpdateFunc(Action<int,RectTransform> func){if (func != null){var f = func;func = (index, rect) =>{f(index + startOffset, rect);};}base.SetUpdateFunc(func);}public void SetItemSizeFunc(Func<int, Vector2> func){if (func != null){var f = func;func = (index) =>{return f(index + startOffset);};}itemSizeFunc = func;}public override void SetItemCountFunc(Func<int> func){realItemCountFunc = func;if (func != null){var f = func;func = () => Mathf.Min(f(), pageSize);}base.SetItemCountFunc(func);}public void SetItemRecycleFunc(Action<RectTransform> func){RecycleFunc = func;}public void SetSelectIndexFunc(Action<int,RectTransform> func){selectIndexFunc = func;}public void SetUpdateCriticalItemsPreprocess(Action func){UpdateCriticalItemsPreprocess = func;}public void SetItemGetAndRecycleFunc(Func<int, RectTransform> getFunc, Action<RectTransform> recycleFunc){if(getFunc != null && recycleFunc != null){itemGetFunc = getFunc;itemRecycleFunc = recycleFunc;}}public void UpdateData(bool immediately = true){if (!initialized){InitScrollView();}if(immediately){willUpdateData |= 3; // 0011InternalUpdateData();}else{if(willUpdateData == 0 && gameObject.active){StartCoroutine(DelayUpdateData());}willUpdateData |= 3;}}public void UpdateDataIncrementally(bool immediately = true){if (!initialized){InitScrollView();}if (immediately){willUpdateData |= 1; // 0001InternalUpdateData();}else{if (willUpdateData == 0){StartCoroutine(DelayUpdateData());}willUpdateData |= 1;}}public void ScrollTo(int index){InternalScrollTo(index);}protected void InternalScrollTo(int index){int count = 0;if (realItemCountFunc != null){count = realItemCountFunc();}index = Mathf.Clamp(index, 0, count - 1);startOffset = Mathf.Clamp(index - pageSize / 2, 0, count - itemCountFunc());UpdateData(true);index = Mathf.Clamp(index, 0, m_dataCount - 1);EnsureItemRect(index);Rect r = managedItems[index].rect;int dir = (int)layoutType & flagScrollDirection;if (dir == 1){// verticalfloat value = 1 - (-r.yMax / (content.sizeDelta.y - refRect.height));//value = Mathf.Clamp01(value);SetNormalizedPosition(value, 1);}else{// horizontalfloat value = r.xMin / (content.sizeDelta.x - refRect.width);//value = Mathf.Clamp01(value);SetNormalizedPosition(value, 0);}}private IEnumerator DelayUpdateData(){yield return null;InternalUpdateData();}protected override void InternalUpdateData(){int newDataCount = 0;bool keepOldItems = ((willUpdateData & 2) == 0);if (itemCountFunc != null){newDataCount = itemCountFunc();}// if (newDataCount != managedItems.Count)if (true){if (managedItems.Count < newDataCount) //增加{if(!keepOldItems){foreach (var itemWithRect in managedItems){// 重置所有rectitemWithRect.rectDirty = true;}}while (managedItems.Count < newDataCount){managedItems.Add(new ScrollItemWithRect());}}else //减少 保留空位 避免GC{for (int i = 0, count = managedItems.Count; i < count; ++i){if(i < newDataCount){// 重置所有rectif(!keepOldItems){managedItems[i].rectDirty = true;}if(i == newDataCount - 1){managedItems[i].rectDirty = true;}}// 超出部分 清理回收itemif (i >= newDataCount){managedItems[i].rectDirty = true;if (managedItems[i].item != null){RecycleOldItem(managedItems[i].item);managedItems[i].item = null;}}}}}else{if(!keepOldItems){for (int i = 0, count = managedItems.Count; i < count; ++i){// 重置所有rectmanagedItems[i].rectDirty = true;}}}m_dataCount = newDataCount;ResetCriticalItems();willUpdateData = 0;}void ResetCriticalItems(){bool hasItem, shouldShow;int firstIndex = -1, lastIndex = -1;for (int i = 0; i < m_dataCount; i++){hasItem = managedItems[i].item != null;shouldShow = ShouldItemSeenAtIndex(i);if (shouldShow){if (firstIndex == -1){firstIndex = i;}lastIndex = i;}if (hasItem && shouldShow){// 应显示且已显示SetDataForItemAtIndex(managedItems[i].item, i);continue;}if (hasItem == shouldShow){// 不应显示且未显示//if (firstIndex != -1)//{//    // 已经遍历完所有要显示的了 后边的先跳过//    break;//}continue;}if (hasItem && !shouldShow){// 不该显示 但是有RecycleOldItem(managedItems[i].item);managedItems[i].item = null;continue;}if (shouldShow && !hasItem){// 需要显示 但是没有RectTransform item = GetNewItem(i);managedItems[i].item = item;OnGetItemForDataIndex(item, i);continue;}}// content.localPosition = Vector2.zero;criticalItemIndex[CriticalItemType.UpToHide] = firstIndex;criticalItemIndex[CriticalItemType.DownToHide] = lastIndex;criticalItemIndex[CriticalItemType.UpToShow] = Mathf.Max(firstIndex - 1, 0);criticalItemIndex[CriticalItemType.DownToShow] = Mathf.Min(lastIndex + 1, m_dataCount - 1);}protected override void SetContentAnchoredPosition(Vector2 position){base.SetContentAnchoredPosition(position);UpdateCriticalItemsPreprocess?.Invoke();UpdateCriticalItems();}protected override void SetNormalizedPosition(float value, int axis){base.SetNormalizedPosition(value, axis);ResetCriticalItems();}RectTransform GetCriticalItem(int type){int index = criticalItemIndex[type];if(index >= 0 && index < m_dataCount){return managedItems[index].item;}return null;}void UpdateCriticalItems(){//if (itemSizeFunc != null)//{//    managedItems.ForEach(item =>//    {//        item.rectDirty = true;//    });//}bool dirty = true;while (dirty){dirty = false;for (int i = CriticalItemType.UpToHide; i <= CriticalItemType.DownToShow; i ++){if(i <= CriticalItemType.DownToHide) //隐藏离开可见区域的item{dirty = dirty || CheckAndHideItem(i);}else  //显示进入可见区域的item{dirty = dirty || CheckAndShowItem(i);}}}}public void ForceUpdateCriticalItems(){// Debug.Log("count : "+managedItems.Count);//// managedItems.ForEach(item =>// {//     item.rectDirty = true;// });//UpdateCriticalItems();}private bool CheckAndHideItem(int criticalItemType){RectTransform item = GetCriticalItem(criticalItemType);int criticalIndex = criticalItemIndex[criticalItemType];if (item != null && !ShouldItemSeenAtIndex(criticalIndex)){RecycleOldItem(item);managedItems[criticalIndex].item = null;//Debug.Log("回收了 " + criticalIndex);if (criticalItemType == CriticalItemType.UpToHide){// 最上隐藏了一个criticalItemIndex[criticalItemType + 2] = Mathf.Max(criticalIndex, criticalItemIndex[criticalItemType + 2]);criticalItemIndex[criticalItemType]++;}else{// 最下隐藏了一个criticalItemIndex[criticalItemType + 2] = Mathf.Min(criticalIndex, criticalItemIndex[criticalItemType + 2]);criticalItemIndex[criticalItemType]--;}criticalItemIndex[criticalItemType] = Mathf.Clamp(criticalItemIndex[criticalItemType], 0, m_dataCount - 1);return true;}return false;}private bool CheckAndShowItem(int criticalItemType){RectTransform item = GetCriticalItem(criticalItemType);int criticalIndex = criticalItemIndex[criticalItemType];//if (item == null && ShouldItemFullySeenAtIndex(criticalItemIndex[criticalItemType - 2]))if (item == null && ShouldItemSeenAtIndex(criticalIndex)){RectTransform newItem = GetNewItem(criticalIndex);OnGetItemForDataIndex(newItem, criticalIndex);//Debug.Log("创建了 " + criticalIndex);managedItems[criticalIndex].item = newItem;if (criticalItemType == CriticalItemType.UpToShow){// 最上显示了一个criticalItemIndex[criticalItemType - 2] = Mathf.Min(criticalIndex, criticalItemIndex[criticalItemType - 2]);criticalItemIndex[criticalItemType]--;}else{// 最下显示了一个criticalItemIndex[criticalItemType - 2] = Mathf.Max(criticalIndex, criticalItemIndex[criticalItemType - 2]);criticalItemIndex[criticalItemType]++;}criticalItemIndex[criticalItemType] = Mathf.Clamp(criticalItemIndex[criticalItemType], 0, m_dataCount - 1);return true;}return false;}bool ShouldItemSeenAtIndex(int index){if(index < 0 || index >= m_dataCount){return false;}EnsureItemRect(index);return new Rect(refRect.position - content.anchoredPosition, refRect.size).Overlaps(managedItems[index].rect);}bool ShouldItemFullySeenAtIndex(int index){if (index < 0 || index >= m_dataCount){return false;}EnsureItemRect(index);return IsRectContains(new Rect(refRect.position - content.anchoredPosition, refRect.size),(managedItems[index].rect));}bool IsRectContains(Rect outRect, Rect inRect, bool bothDimensions = false){if (bothDimensions){bool xContains = (outRect.xMax >= inRect.xMax) && (outRect.xMin <= inRect.xMin);bool yContains = (outRect.yMax >= inRect.yMax) && (outRect.yMin <= inRect.yMin);return xContains && yContains;}else{int dir = (int)layoutType & flagScrollDirection;if(dir == 1){// 垂直滚动 只计算y向return (outRect.yMax >= inRect.yMax) && (outRect.yMin <= inRect.yMin);}else // = 0{// 水平滚动 只计算x向return (outRect.xMax >= inRect.xMax) && (outRect.xMin <= inRect.xMin);}}}void InitPool(){GameObject poolNode = new GameObject("POOL");poolNode.SetActive(false);poolNode.transform.SetParent(transform,false);itemPool = new SimpleObjPool<RectTransform>(poolSize,(RectTransform item) => {// 回收item.transform.SetParent(poolNode.transform,false);},() => {// 构造GameObject itemObj = Instantiate(itemTemplate.gameObject);//设置元素的滚动视图组件(即this)if (itemObj.GetComponent<ScrollViewExItem>()){itemObj.GetComponent<ScrollViewExItem>().scrollView = this;}RectTransform item = itemObj.GetComponent<RectTransform>();itemObj.transform.SetParent(poolNode.transform,false);item.anchorMin = Vector2.up;item.anchorMax = Vector2.up;item.pivot = Vector2.zero;//rectTrans.pivot = Vector2.up;itemObj.SetActive(true);return item;});}void OnGetItemForDataIndex(RectTransform item, int index){SetDataForItemAtIndex(item, index);item.transform.SetParent(content, false);}void SetDataForItemAtIndex(RectTransform item, int index){if (updateFunc != null)updateFunc(index,item);SetPosForItemAtIndex(item,index);}void SetPosForItemAtIndex(RectTransform item, int index){EnsureItemRect(index);var managedItem = managedItems[index];if (managedItem.item != null && managedItem.item.GetComponent<ScrollViewExItem>()){item.GetComponent<ScrollViewExItem>().itemIndex = index;}Rect r = managedItem.rect;item.localPosition = r.position;item.sizeDelta = r.size;}Vector2 GetItemSize(int index,ScrollItemWithRect item){if(index >= 0 && index <= m_dataCount){if (itemSizeFunc != null){return itemSizeFunc(index);}}return defaultItemSize;}private RectTransform GetNewItem(int index){RectTransform item;if(itemGetFunc != null){item = itemGetFunc(index);}else{item = itemPool.Get();}return item;}private void RecycleOldItem(RectTransform item){if (itemRecycleFunc != null){itemRecycleFunc(item);}else{itemPool.Recycle(item);}if (RecycleFunc != null){RecycleFunc(item);}}void InitScrollView(){initialized = true;// 根据设置来控制原ScrollRect的滚动方向int dir = (int)layoutType & flagScrollDirection;content.pivot = Vector2.up;InitPool();UpdateRefRect();}Vector3[] viewWorldConers = new Vector3[4];Vector3[] rectCorners = new Vector3[2];void UpdateRefRect(){/**  WorldCorners* *    1 ------- 2     *    |         |*    |         |*    0 ------- 3* */// refRect是在Content节点下的 viewport的 rectviewRect.GetWorldCorners(viewWorldConers);rectCorners[0] = content.transform.InverseTransformPoint(viewWorldConers[0]);rectCorners[1] = content.transform.InverseTransformPoint(viewWorldConers[2]);refRect = new Rect((Vector2)rectCorners[0] - content.anchoredPosition, rectCorners[1] - rectCorners[0]);}void MovePos(ref Vector2 pos, Vector2 size){// 注意 所有的rect都是左下角为基准switch (layoutType){case ItemLayoutType.Vertical:// 垂直方向 向下移动pos.y -= size.y;break;case ItemLayoutType.Horizontal:// 水平方向 向右移动pos.x += size.x;break;case ItemLayoutType.VerticalThenHorizontal:pos.y -= size.y;if (pos.y <= -(refRect.height - size.y / 2)){pos.y = 0;pos.x += size.x;}break;case ItemLayoutType.HorizontalThenVertical:pos.x += size.x;if(pos.x >= refRect.width - size.x / 2){pos.x = 0;pos.y -= size.y;}break;default:break;}}protected void EnsureItemRect(int index){if (!managedItems[index].rectDirty){// 已经是干净的了return;}ScrollItemWithRect firstItem = managedItems[0];if (firstItem.rectDirty){Vector2 firstSize = GetItemSize(0, firstItem);firstItem.rect = CreateWithLeftTopAndSize(Vector2.zero, firstSize);firstItem.rect.position += defaultItemSpace;firstItem.rectDirty = false;if (firstItem.item){firstItem.item.localPosition = firstItem.rect.position;}}// 当前item之前的最近的已更新的rectint nearestClean = 0;for (int i = index; i >= 0; --i){if (!managedItems[i].rectDirty){nearestClean = i;break;}}// 需要更新 从 nearestClean 到 index 的尺寸Rect nearestCleanRect = managedItems[nearestClean].rect;Vector2 curPos = GetLeftTop(nearestCleanRect);Vector2 size = nearestCleanRect.size;MovePos(ref curPos, size);for (int i = nearestClean + 1; i <= index; i++){size = GetItemSize(i, managedItems[i]);managedItems[i].rect = CreateWithLeftTopAndSize(curPos, size);managedItems[i].rect.position += defaultItemSpace;managedItems[i].rectDirty = false;MovePos(ref curPos, size);if (managedItems[i].item){managedItems[i].item.localPosition = managedItems[i].rect.position;}}Vector2 range = new Vector2(Mathf.Abs(curPos.x), Mathf.Abs(curPos.y));switch (layoutType){case ItemLayoutType.VerticalThenHorizontal:range.x += size.x;range.y = refRect.height;break;case ItemLayoutType.HorizontalThenVertical:range.x = refRect.width;if (curPos.x != 0){range.y += size.y;}break;default:break;}content.sizeDelta = range;}//选择Itempublic void SelectItem(int index){for (int i = 0; i < managedItems.Count; i++){var managedItem = managedItems[i];if (managedItem != null && managedItem.item != null && managedItem.item.GetComponent<ScrollViewExItem>()){ScrollViewExItem item = managedItem.item.GetComponent<ScrollViewExItem>();item.SetSelected(item.itemIndex == index);if (item.itemIndex == index && selectIndexFunc != null){selectIndexFunc(index, managedItem.item);}}}}public UnityEvent<int, ScrollViewExItem> onClickItem => _onClickItem ?? (_onClickItem = new UnityEvent<int, ScrollViewExItem>());private static Vector2 GetLeftTop(Rect rect){Vector2 ret = rect.position;ret.y += rect.size.y;return ret;}private static Rect CreateWithLeftTopAndSize(Vector2 leftTop, Vector2 size){Vector2 leftBottom = leftTop - new Vector2(0,size.y);//Debug.Log(" leftBottom : "+leftBottom +" size : "+size );return new Rect(leftBottom,size);}protected override void OnDestroy(){if (itemPool != null){itemPool.Purge();}}protected Rect GetItemLocalRect(int index){if(index >= 0 && index < m_dataCount){EnsureItemRect(index);return managedItems[index].rect;}return new Rect();}protected override void Awake(){base.Awake();onValueChanged.AddListener(OnValueChanged);}private void Update(){if (Input.GetMouseButtonUp(0) || Input.GetMouseButtonDown(0))canNextPage = true;}bool reloadFlag = false;private void OnValueChanged(Vector2 position){if (reloadFlag){UpdateData(true);reloadFlag = false;}if (Input.GetMouseButton(0) && !canNextPage) return;int toShow;int critical;bool downward;int pin;if (((int)layoutType & flagScrollDirection) == 1){// 垂直滚动 只计算y向if (velocity.y > 0){// 向上toShow = criticalItemIndex[CriticalItemType.DownToShow];critical = pageSize - 1;if (toShow < critical){return;}pin = critical - 1;downward = false;}else{// 向下toShow = criticalItemIndex[CriticalItemType.UpToShow];critical = 0;if (toShow > critical){return;}pin = critical + 1;downward = true;}}else // = 0{// 水平滚动 只计算x向if (velocity.x > 0){// 向右toShow = criticalItemIndex[CriticalItemType.UpToShow];critical = 0;if (toShow > critical){return;}pin = critical + 1;downward = true;}else{// 向左toShow = criticalItemIndex[CriticalItemType.DownToShow];critical = pageSize - 1;if (toShow < critical){return;}pin = critical - 1;downward = false;}}// 翻页int old = startOffset;if (downward){startOffset -= pageSize / 2;}else{startOffset += pageSize / 2;}canNextPage = false;int realDataCount = 0;if (realItemCountFunc != null){realDataCount = realItemCountFunc();}startOffset = Mathf.Clamp(startOffset, 0, Mathf.Max(realDataCount - pageSize, 0));if (old != startOffset){reloadFlag = true;// 计算 pin元素的世界坐标Rect rect = GetItemLocalRect(pin);Vector2 oldWorld = content.TransformPoint(rect.position);UpdateData(true);int dataCount = 0;if (itemCountFunc != null){dataCount = itemCountFunc();}if (dataCount > 0){EnsureItemRect(0);if (dataCount > 1){EnsureItemRect(dataCount - 1);}}// 根据 pin元素的世界坐标 计算出content的positionint pin2 = pin + old - startOffset;Rect rect2 = GetItemLocalRect(pin2);Vector2 newWorld = content.TransformPoint(rect2.position);Vector2 deltaWorld = newWorld - oldWorld;Vector2 deltaLocal = content.InverseTransformVector(deltaWorld);SetContentAnchoredPosition(content.anchoredPosition - deltaLocal);UpdateData(true);// 减速velocity /= 50f;}}
}

ScrollViewExItem.cs

using UnityEngine;public class ScrollViewExItem : MonoBehaviour
{public ScrollViewEx scrollView;public int itemIndex;public bool isSelected;public void SetSelected(bool value){isSelected = value;OnSelected();}//选择监听方法public virtual void OnSelected(){}//点击监听方法public virtual void OnClick(){scrollView.onClickItem.Invoke(itemIndex, this);}
}

还有一个工具类脚本,SimpleObjPool.cs。

using System;
using System.Collections.Generic;public class SimpleObjPool<T>
{private readonly Stack<T> m_Stack;private readonly Func<T> m_ctor;private readonly Action<T> m_OnRecycle;private int m_Size;private int m_UsedCount;public SimpleObjPool(int max = 5, Action<T> actionOnReset = null, Func <T> ctor = null){m_Stack = new Stack<T>(max);m_Size = max;m_OnRecycle = actionOnReset;m_ctor = ctor;}public T Get(){T item;if (m_Stack.Count == 0){if(null != m_ctor){item = m_ctor();}else{item = Activator.CreateInstance<T>();}}else{item = m_Stack.Pop();}m_UsedCount++;return item;}public void Recycle(T item){if(m_OnRecycle!= null){m_OnRecycle.Invoke(item);}if(m_Stack.Count < m_Size){m_Stack.Push(item);}m_UsedCount -- ;}/*public T GetAndAutoRecycle(){T obj = Get();Utils.OnNextFrameCall(()=> { Recycle(obj); });return obj;}*/public void Purge(){// TODO}public override string ToString(){return string.Format("SimpleObjPool: item=[{0}], inUse=[{1}], restInPool=[{2}/{3}] ", typeof(T), m_UsedCount, m_Stack.Count, m_Size);}}

以上三个脚本的代码就不一一细说了,大家可以参考。

至此,我们的滚动视图复用框架就完成了。

示例

示例代码

接下来贴出使用的组件截图和使用脚本示例代码。

使用的实力代码脚本为UIBoxDetail.cs和UIBoxDetailItem.cs。

using System.Collections.Generic;
using Msg;
using UnityEngine;
using UnityEngine.UI;/// <summary>
/// 宝箱详情UI
/// </summary>
public class UIBoxDetail : BasePanel
{public const string BoxList = "UI_Event_BoxList";// 宝箱列表public const string UnlockBox = "UI_Event_UnlockBox";// 解锁宝箱public const string ReduceTime = "UI_Event_ReduceTime";// 扣减广告加速时间public RectTransform coinDiamondRoot;public Button btnBack;/// <summary>/// 宝箱背景类型/// </summary>public enum BgType{None,// 无宝箱Lock,// 未解锁SpeedUp,// 加速Get,// 领取Overflow// 已满}public ScrollViewEx detailSrEx;private void OnEnable(){EventMgr.GetInstance().AddEventListener<BoxOpenResponse>(BoxEvent.BoxOpenResponse, OnBoxOpenResponse);}private void OnDisable(){EventMgr.GetInstance().RemoveEventListener<BoxOpenResponse>(BoxEvent.BoxOpenResponse, OnBoxOpenResponse);}protected override void Awake(){detailSrEx.UpdateData(false);detailSrEx.SetUpdateFunc((index, rectTransform) =>{UIBoxDetailItem item = rectTransform.GetComponent<UIBoxDetailItem>();item.OnRefresh(BoxModel.BoxList[index]);});detailSrEx.SetItemCountFunc(() => BoxModel.BoxList.Count);}private void Start(){BoxMgr.GetInstance().BoxListReq();UIMgr.GetInstance().ShowInnerRes(coinDiamondRoot, new List<TopInnerResDataVo>{new TopInnerResDataVo(E_TopInnerRes.Coin, PersonalInfoModel.Player.NumGold),new TopInnerResDataVo(E_TopInnerRes.Diamond, PersonalInfoModel.Player.NumStone)});txtClose.text = MultilingualUtil.MultilingualText(29);btnBack.onClick.AddListener(HideMe);}public override void Notify(string msgType, object msgData){base.Notify(msgType, msgData);switch (msgType){case BoxList:case UnlockBox:case ReduceTime:RefreshBoxList(msgData as Box[]);break;}}private void RefreshBoxList(Box[] boxes){detailSrEx.UpdateData(false);detailSrEx.SetUpdateFunc((index, rectTransform) =>{UIBoxDetailItem item = rectTransform.GetComponent<UIBoxDetailItem>();item.OnRefresh(boxes[index]);});detailSrEx.SetItemCountFunc(() => boxes.Length);}#region responseprivate void OnBoxOpenResponse(BoxOpenResponse response){detailSrEx.SetUpdateFunc((index, rectTransform) =>{rectTransform.name = index.ToString();});detailSrEx.SetItemCountFunc(() => BoxModel.BoxList.Count);}#endregion[Header("---- 多语言控件 ----")]public Text txtClose;
}
using System.Text;
using Msg;
using UnityEngine;
using UnityEngine.UI;public class UIBoxDetailItem : ScrollViewExItem
{public RectTransform timeGroup;public Image imgBg;public Image imgIcon;public Image imgMask;public Text txtTime;public Text txtTips;public Text txtEmpty;public Text txtTitle;public Button btnTitle;private StringBuilder _iconPath = new StringBuilder();// icon路径private StringBuilder _titleBuilder = new StringBuilder();private UIBoxDetail.BgType _selectedType;// 当前选中宝箱private Timer _timer;private Timer _timerUpdate;private long _countdownStamp;// 倒计时时间private bool _isTimeGroup;// 是否启用时间组件private bool _isTime;// 是否启用时间文本UIprivate bool _isIcon;// 是否启用Iconprivate void Start(){_isTimeGroup = false;_isTime = false;_isIcon = false;_selectedType = UIBoxDetail.BgType.None;// 默认无btnTitle.onClick.AddListener(() =>{BoxModel.SetBox(BoxModel.BoxList[itemIndex]);switch (_selectedType){case UIBoxDetail.BgType.Lock:case UIBoxDetail.BgType.SpeedUp:UIMgr.GetInstance().ShowPanel<UIBoxOpen>(UIDef.UI_BOXOPEN, BoxModel.BoxList[itemIndex]);break;case UIBoxDetail.BgType.Get:// 直接领取奖励BoxMgr.GetInstance().BoxClaimRewardReq(BoxModel.BoxList[itemIndex].BoxID, BoxModel.BoxList[itemIndex].ID);break;}});}private void OnDestroy(){_timer?.Stop();_timerUpdate?.Stop();}public void OnRefresh(Box data){_timer?.Stop();_timerUpdate?.Stop();// 创建新角色没匹配时,宝箱列表没有长度if (data == null){RefreshContent(UIBoxDetail.BgType.None, null);return;}// 新角色匹配后,宝箱列表有长度if (data.ID != string.Empty && data.BoxID == 0){RefreshContent(UIBoxDetail.BgType.None, data);return;}if (data.ID == string.Empty && data.BoxID == 0){RefreshContent(UIBoxDetail.BgType.None, data);return;}var boxCfg = ConfigManager._BoxCfgMgr.GetDataByID((int)data.BoxID);var second = BoxMgr.GetInstance().CalculateSecond(boxCfg.LifeTime);if (data.UnlockTimeStamp == 0)// 未解锁{RefreshContent(UIBoxDetail.BgType.Lock, data);txtTime.text = second > 10? second + MultilingualUtil.MultilingualText(426): second + MultilingualUtil.MultilingualText(280);}else if (data.UnlockTimeStamp > 0 && TimeUtil.GetUnixTimeStamp() < data.UnlockTimeStamp)// 加速{RefreshContent(UIBoxDetail.BgType.SpeedUp, data);_countdownStamp = data.UnlockTimeStamp - TimeUtil.GetUnixTimeStamp() - data.ReduceTime;// 当前宝箱时间戳小于_timer = new Timer(1f, true, () =>{_countdownStamp--;if (txtTime != null)txtTime.text = TimeUtil.FormatTime(_countdownStamp);});_timer.Start();_timerUpdate = new Timer(Time.deltaTime, true, () =>{if (_countdownStamp <= 0){BoxMgr.GetInstance().BoxListReq();// 重新请求宝箱列表BoxModel.SetHasSpeedUp(false);_timer?.Stop();_timerUpdate?.Stop();}});_timerUpdate.Start();}else if (data.UnlockTimeStamp > 0 && TimeUtil.GetUnixTimeStamp() > data.UnlockTimeStamp)// 可领取{RefreshContent(UIBoxDetail.BgType.Get, data);}if (data.UnlockTimeStamp == 0)txtTime.text = second > 10? second + MultilingualUtil.MultilingualText(426): second + MultilingualUtil.MultilingualText(280);elsetxtTime.text = TimeUtil.FormatTime(_countdownStamp);imgIcon.sprite = AssetBundleMgr.GetInstance().LoadUISprite(data.BoxID != 0 ? boxCfg.Icon : "");}/// <summary>/// 刷新内容/// </summary>/// <param name="type">类型</param>/// <param name="data">宝箱数据</param>private void RefreshContent(UIBoxDetail.BgType type, Box data){_iconPath.Clear();_titleBuilder.Clear();switch (type){case UIBoxDetail.BgType.None: // 无宝箱_isTime = false;_isTimeGroup = false;_isIcon = false;_selectedType = UIBoxDetail.BgType.None;_iconPath.Append("UIAtlas/Box/empty_btn");break;case UIBoxDetail.BgType.Lock: // 未解锁_isTime = true;_isTimeGroup = false;_isIcon = true;_selectedType = UIBoxDetail.BgType.Lock;_titleBuilder.Append(MultilingualUtil.MultilingualText(85));_iconPath.Append("UIAtlas/Box/treasure02_btn");break;case UIBoxDetail.BgType.SpeedUp: // 加速_isTime = true;_isTimeGroup = true;_isIcon = true;_selectedType = UIBoxDetail.BgType.SpeedUp;_titleBuilder.Append(MultilingualUtil.MultilingualText(86));_iconPath.Append("UIAtlas/Box/treasure01_btn");break;case UIBoxDetail.BgType.Get: // 领取奖励_isTime = false;_isTimeGroup = false;_isIcon = true;_selectedType = UIBoxDetail.BgType.Get;_titleBuilder.Append(MultilingualUtil.MultilingualText(87));_iconPath.Append("UIAtlas/Box/open_btn");break;}var boxCfg = ConfigManager._BoxCfgMgr.GetDataByID((int)data.BoxID);if (boxCfg != null)txtTips.text = BoxCfgMgr.Instance.GetMultiLangName(boxCfg);txtTitle.text = _titleBuilder.ToString();txtEmpty.text = MultilingualUtil.MultilingualText(84);imgBg.sprite = AssetBundleMgr.GetInstance().LoadUISprite(_iconPath.ToString());imgIcon.gameObject.SetActive(_isIcon);txtTime.gameObject.SetActive(data.BoxID != 0 && _isTime);timeGroup.gameObject.SetActive(_isTimeGroup);imgMask.gameObject.SetActive(data.BoxID != 0 && data.ReduceTime != 0);txtTips.gameObject.SetActive(data.BoxID != 0);txtEmpty.gameObject.SetActive(data.BoxID == 0);btnTitle.gameObject.SetActive(data.BoxID != 0);}
}
示例组件截图

itemTemplate需要指定一个有UIBoxDetailItem脚本的显示对象,如下图所示。

最后

其中还有更多的细节,就未能一一提及。

当然还有更多有待优化的逻辑,需要大家来指出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/34300.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

13个行业数据分析指标体系如何建设100问

提供针对13个行业的数据分析指标体系的全面指南&#xff0c;涵盖各行业的关键指标和分析维度&#xff0c;帮助读者深入了解和构建有效的指标体系。以下是文章的主要内容&#xff1a; 电商行业数据指标体系&#xff1a;包括客户价值、商品、网站流量、整体运营、市场营销活动、市…

若依-前后端分离项目学习

第一天&#xff08;6.24&#xff09; 具体参考视频 b站 楠哥教你学Java 【【开源项目学习】若依前后端分离版&#xff0c;通俗易懂&#xff0c;快速上手】 https://www.bilibili.com/video/BV1HT4y1d7oA/?share_sourcecopy_web&vd_sourcecd9334b72b49da3614a4257…

C++如何实现继承和多态

继承 继承是指一个类&#xff08;子类&#xff09;从另一个类&#xff08;父类&#xff09;继承属性和方法。C支持单继承和多继承。 #include <iostream>// 基类&#xff08;父类&#xff09; class Animal { public:// 基类中的方法void eat() {std::cout << &q…

Elasticsearch Scroll 报错entity content is too long

2024-06-24 15:22:01:568 ERROR [task-31] (ScrollFetcherProduceAction.java:129) 访问ES出错org.apache.http.ContentTooLongException: entity content is too long [112750110] for the configured buffer limit [104857600]at org.elasticsearch.client.HeapBufferedAsync…

一下出来4个面试官,这是要舌战群儒啊

老张昨天下午请假了&#xff0c;我猜他就是面试去了。果不其然&#xff0c;今天来了&#xff0c;我问老张&#xff1a;昨天面试如何&#xff1f;老张很惊讶的问&#xff1a;你怎么知道我面试去了&#xff1f;我迫不及待的说&#xff1a;赶紧说说昨天面试的场景&#xff0c;给我…

智慧安防/边缘计算EasyCVR视频汇聚网关:EasySearch无法探测到服务器如何处理?

安防监控EasyCVR智能边缘网关/视频汇聚网关/视频网关属于软硬一体的边缘计算硬件&#xff0c;可提供多协议&#xff08;RTSP/RTMP/国标GB28181/GAT1400/海康Ehome/大华/海康/宇视等SDK&#xff09;的设备接入、音视频采集、视频转码、处理、分发等服务&#xff0c;系统具备实时…

Redis-事务-watch-unwatch

文章目录 1、监视key2、提交事务 1、监视key 打开两个窗口&#xff0c;第一个窗口先监视key&#xff0c;然后开始事务&#xff0c;然后再打开第二个窗口&#xff0c;修改balance为0 2、提交事务 此时事务被打断

playwright vscode 插件源码解析

Playwright vscode插件主要功能 Playwright是微软开发的一款主要用于UI自动化测试的工具&#xff0c;在vscode中上安装playwright vscode插件&#xff0c;可以运行&#xff0c;录制UI自动化测试。 playwright vscode插件主要包括两块功能&#xff0c;功能一是在Test Explorer中…

探索 Java 死锁:常见原因与解决方案

什么是死锁&#xff1f; 死锁是一种特殊的情况&#xff0c;发生在两个或多个线程彼此等待对方持有的资源&#xff0c;从而陷入无限等待的状态。具体而言&#xff0c;死锁通常涉及以下四个必要条件&#xff1a; 互斥条件&#xff1a;至少有一个资源被一个线程独占。持有并等待…

解决Microsoft Edge浏览器无法使用英文翻译功能

一、问题描述 原来我们使用的Microsoft Edge浏览器是可以对英文界面选择翻译为中文的&#xff1b;但是最近该浏览器更新过后右上角的翻译图标找不到了&#xff0c;无法翻译英文界面内容。 二、解决方法 2.1、打开浏览器的设置界面 2.2、选择语言 2.3、将首选语言下除中文外的…

【2024德国工作】蓝卡攻略:人在中国,怎么去德国工作?

德国工作签证解析 外国人只要拥有符合德国劳动法的劳动合同&#xff0c;工资符合当地标准&#xff08;非紧缺专业&#xff0c;税前工资一般需达到49600欧元&#xff09;&#xff0c;并且具备一定的外语能力&#xff0c;就可以申请德国境内工作签证&#xff01;不申请者还需要有…

【electron 5】electron将获取的Uint8Array转为中文

使用protobufjs&#xff1a; 安装&#xff1a; yarn add global protobufjs yarn add global protobufjs-cli// 将项目中的*.proto文件编译成json文件 npx pbjs -t json electron/main/proto/*.proto > electron/main/proto/proto.json可以在src/proto目录下生成一个proto.…

国内Mac安装Homebrew方法

文章目录 前言步骤 参考&#xff1a;https://blog.csdn.net/itwangyang520/article/details/134125435 前言 今天尝试下载git&#xff0c;官方给的建议是使用Homebrew下载&#xff0c;但发现新电脑里没有&#xff0c;于是尝试Homebrew下载&#xff0c;但发现Homebrew不存在&am…

黑曼巴精神不死!Mamba 2 出世,性能狂飙8倍

年前&#xff0c;Mamba 被 ICLR 拒稿的消息曾引起轩然大波。然而&#xff0c;Mamba作者在6月初又发布了 Mamba 2 架构&#xff0c;这次&#xff0c;Mamba-2 顺利地拿下 ICML。就连 Nvidia 都被吸引&#xff0c;都用它重新训练了GPT3模型。 Transformer vs Mamba vs Mamba2 : 比…

一文读懂交换机MAC地址表:五大关键点,图解21步

HCIA 新班开课了华为HCIA课程介绍苏州面授班 | 全国直播班循环开班&#xff0c;免费重学前言 什么是MAC地址表?MAC地址表有什么作用&#xff1f;MAC地址表里面包含了哪些要素&#xff1f;今天带你好好唠唠。 我们以一个案例为例&#xff1a; 如上图&#xff1a;PC1和PC2通…

C#——堆栈和队列详情

堆栈和队列 堆栈 堆栈类表示一个后进先出的对象集合&#xff0c;当需要对项目进行后进先出的访问时&#xff0c;则可以使用堆栈。向堆栈中添加元素称为推入元素&#xff0c;从堆栈中移除元素称为弹出元素。 关键字: Stack stack常用属性 : 属性 描述Count获取堆栈中包含的…

Javascript高级程序设计(第四版)--学习记录之变量、内存

原始值与引用值 原始值&#xff1a;简单的数据即基础数据类型&#xff0c;按值访问。 引用值&#xff1a;由多个值构成的对象即复杂数据类型&#xff0c;按引用访问。 动态属性 对于引用值而言&#xff0c;可以随时添加、修改和删除其属性和方法。 let person new Object(); p…

昇思25天学习打卡营第1天|认识MindSpore

MindSpore 基本介绍 昇思MindSpore是一个全场景深度学习框架&#xff0c;旨在实现易开发、高效执行、全场景统一部署三大目标。 易开发&#xff1a;API友好、调试难度低高效执行&#xff1a;包括计算效率、数据预处理效率和分布式训练效率全场景&#xff1a;框架同时支持云、…

LabVIEW操作系列

目的&#xff1a;初学LabVIEW&#xff0c;做记录 文章目录 前言一、LabVIEW操作界面1.1 新建vi1.2 控件1.3 加法案例 二、编程特点2.1 特点2.2 实现按顺序执行 三、程序结构3.1 平铺式顺序结构3.2 循环结构3.2.1 For循环3.2.2 While循环3.2.3 中途停止For循环3.2.4 中途停止Whi…

区块链实验室(37) - 交叉编译百度xuperchain for arm64

纠结了很久&#xff0c;终于成功编译xuperchain for arm64。踩到1个坑&#xff0c;说明如下。 1、官方文档是这么说的&#xff1a;go语言版本推荐1.5-1.8 2、但是同一个页面&#xff0c;又是这么说的&#xff1a;不推荐使用1.11之前的版本。 3、问题来了&#xff1a;用什么版本…