发现问题
在游戏开发中有一个常见的需求,就是需要在屏幕显示多个(多达上百)显示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的组件中完成这个功能。
解决前两个问题
框架代码
首先,对于前两个问题,我们来做一个简单的自定义滚动视图(先不处理复用的逻辑)。
cs
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
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。
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;// 道具id
public 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代码:
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 show
public enum ItemLayoutType
{
// 最后一位表示滚动方向
Vertical = 1, // 0001
Horizontal = 2, // 0010
VerticalThenHorizontal = 4, // 0100
HorizontalThenVertical = 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个临界index
protected int[] criticalItemIndex = new int[4];
Rect refRect;
// resource management
SimpleObjPool<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;
// status
private 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; // 0011
InternalUpdateData();
}
else
{
if(willUpdateData == 0 && gameObject.active)
{
StartCoroutine(DelayUpdateData());
}
willUpdateData |= 3;
}
}
public void UpdateDataIncrementally(bool immediately = true)
{
if (!initialized)
{
InitScrollView();
}
if (immediately)
{
willUpdateData |= 1; // 0001
InternalUpdateData();
}
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)
{
// vertical
float value = 1 - (-r.yMax / (content.sizeDelta.y - refRect.height));
//value = Mathf.Clamp01(value);
SetNormalizedPosition(value, 1);
}
else
{
// horizontal
float 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)
{
// 重置所有rect
itemWithRect.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)
{
// 重置所有rect
if(!keepOldItems)
{
managedItems[i].rectDirty = true;
}
if(i == newDataCount - 1)
{
managedItems[i].rectDirty = true;
}
}
// 超出部分 清理回收item
if (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)
{
// 重置所有rect
managedItems[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的 rect
viewRect.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之前的最近的已更新的rect
int 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;
}
//选择Item
public 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的position
int 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
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。
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。
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 response
private void OnBoxOpenResponse(BoxOpenResponse response)
{
detailSrEx.SetUpdateFunc((index, rectTransform) =>
{
rectTransform.name = index.ToString();
});
detailSrEx.SetItemCountFunc(() => BoxModel.BoxList.Count);
}
#endregion
[Header("---- 多语言控件 ----")]
public Text txtClose;
}
cs
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;// 是否启用时间文本UI
private bool _isIcon;// 是否启用Icon
private 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);
else
txtTime.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脚本的显示对象,如下图所示。
最后
其中还有更多的细节,就未能一一提及。
当然还有更多有待优化的逻辑,需要大家来指出。