在做demo的时候遇到了滚动列表,开始为了实现点击切换模型的功能,选择在编辑器中直接添加,但是这种方式很麻烦也不灵活。
那么我希望左侧滚动区域内的子项数量,根据数据自动生成,并且在点击的时候,能够切换模型显示。
下面是我整理的关于滚动列表组件是实现过程:
1.首先需要先定义这个列表的布局属性:行数、列数、子项间距、布局方向,有了这些才能计算子项的位置。这些属性我们希望在编辑器中能被设置,但是不希望外部对象能够访问,所以需要使用[SerializeField]关键字修饰。
enum Direction
{
Horizontal,//水平
Vertical//垂直
}
[SerializeField]
private RectTransform cell;//子项
[SerializeField]
private Vector2 cell_gap;//行、列间距
[SerializeField, Range(1, 50)]//行数
private int row;
[SerializeField, Range(1, 50)]//列数
private int col;
[SerializeField]
private Direction direction = Direction.Horizontal;//方向
2.生成子项。子项也不需要做成一个prefab,在滚动内容中创建一个,作为参数传给组件即可。
private RectTransform createItem()
{
RectTransform item = GameObject.Instantiate(cell);//创建item,并重置坐标
item.SetParent(cacheRect, false);
item.anchorMax = Vector2.up;
item.anchorMin = Vector2.up;
item.pivot = new Vector2(0.5f, 0.5f);
item.anchoredPosition3D = Vector3.zero;
item.gameObject.SetActive(true);
if (onCreateItem != null)
onCreateItem(createIndex, item.gameObject);
createIndex++;
return item;
}
3.计算子项位置,根据子项索引和布局信息来确定在第几行第几列。有一点需要注意的是,布局算的是左上角的坐标,而游戏物体的坐标是使用中心点计算的,所以计算出来的坐标需要加上中心点的偏移值,也就是物体宽高的一半。
private Vector2 GetPosByIndex(int index)
{
if (index < 0)
index = 0;
float x;
float y;
if (direction == Direction.Horizontal)
{
x = index / row;
y = index % row;
}
else
{
x = index % col;
y = index / col;
}
Vector2 nativeSize = cell.sizeDelta;
x = x * cellSize.x + nativeSize.x * 0.5f;
y = -(y * cellSize.y + nativeSize.y * 0.5f);
return new Vector2(x, y);
}
//子项宽高 + 布局间距
private Vector2 cellSize { get { return cell.sizeDelta + cell_gap; } }
4.添加完子项,需要对容器尺寸重新计算,算出最大行数和最大列数,确保子项在内容区域。
void resetBounds()
{
int m_row = row;
int m_col = col;
if (direction == Direction.Horizontal)
{
m_col = itemCounts / row;
if (itemCounts % row != 0)
m_col++;
m_col = Mathf.Max(m_col, col - 1);
}
else
{
m_row = itemCounts / col;
if (itemCounts % col != 0)
m_row++;
m_row = Mathf.Max(m_row, row - 1);
}
cacheRect.sizeDelta = new Vector2(m_col * cellSize.x - cell_gap.x, m_row * cellSize.y - cell_gap.y);
}
5.子项数据更新,数据更新使用回调的方式,让组件与逻辑代码解耦。注意到上方创建的时候有调用onCreateItem回调方法,就是在组件中定义,使用者传入具体的回调方法,在创建和数据更新的时候回调给使用者,使用者根据子项的索引,去更新数据显示,这样我们的组件完全不会涉及业务逻辑。
//组件中定义委托回调事件
public delegate void UILoopCallBack(int index, GameObject go);
public UILoopCallBack onCreateItem;//创建子项回调
public UILoopCallBack onUpdateItem;//更新子项回调
//使用组件的地方,注册
uilist.onCreateItem = onCreateItem;
uilist.onUpdateItem = onUpdateItem;
//子项创建回调
void onCreateItem(int index,GameObject go)
{
//Debug.Log("onCreateItem " + index);
Button btn = go.GetComponent<Button>();
if(btn)
{
btn.onClick.AddListener(() =>
{
changeAnimModel(index);
});
}
updateItemInfo(index, go);
}
//子项数据更新回调
void onUpdateItem(int index, GameObject go)
{
//Debug.Log("onUpdateItem " + index);
updateItemInfo(index, go);
}
//更新子项显示信息
void updateItemInfo(int index, GameObject go)
{
Text text = go.transform.Find("Text").GetComponent<Text>();
var key = nameList[index];
text.text = nameDic[key];
}
6.以上一个动态创建子项的列表组件的创建、布局、数据更新等基本功能就完成了,我们还需要一个对外的函数,来设置列表需要显示的数量。
private int itemCounts;
public int ItemCounts
{
get
{
return itemCounts;
}
set
{
itemCounts = value;
updateAll();
}
}
void updateAll()
{
clearShowItems();
addShowItems();
resetBounds();
}
//移除之前列表中子项显示
void clearShowItems()
{
var count = showItems.Count;
if (count > 0)
{
for (var i = count - 1; i >= 0; i--)
{
showItems[i].position = InvalidPos;
}
showItems.Clear();
}
}
//生成子项
void addShowItems()
{
for (var i = 0; i < itemCounts; i++)
{
RectTransform rectItem = getItemFromPool();
rectItem.anchoredPosition = GetPosByIndex(i);
showItems.Add(rectItem);
if (onUpdateItem != null)
onUpdateItem(i, rectItem.gameObject);
}
}
7.在调用的地方,只需要设置列表长度即可。
//设置组件显示长度为数据长度
uilist.ItemCounts = nameList.Count;
8.以上一个动态创建子项的列表组件的核心逻辑已完成。但是如果每次更新列表都将之前的子项全部移除,在创建新的子项,那么会造成一些无意义的消耗。所以我们将列表中创建出来的子项,像对象池一样缓存在组件中,在需要显示子项的时候如果对象池中有,那就不需要在创建。所以我们需要引入一个list来缓存创建过的子项。
void clearShowItems()
{
var count = showItems.Count;
if (count > 0)
{
for (var i = count - 1; i >= 0; i--)
{
showItems[i].position = InvalidPos;
poolItems.Add(showItems[i]);//移除的时候放进list中缓存
}
showItems.Clear();
}
}
//获取子项,优先从缓存中获取
private RectTransform getItemFromPool()
{
if (poolItems.Count > 0)
{
RectTransform rect = poolItems[poolItems.Count - 1];
poolItems.RemoveAt(poolItems.Count - 1);
return rect;
}
return createItem();
}
当然这个列表组件还能继续优化,如图再往下列表中完全看不见的位置也会生成子项,如果往后还有100个子项,那它也会一次性生成出来,严重影响性能。这些子项只是数据不同,完全可以复用,只需要保持窗口显示的最大数量+1个,动态更新数据一定能实现,在之后的文章中我也会继续优化这个组件。
列表组件代码
public class UIList : MonoBehaviour
{
enum Direction
{
Horizontal,//水平
Vertical//垂直
}
[SerializeField]
private RectTransform cell;//子项
[SerializeField]
private Vector2 cell_gap;//行、列间距
[SerializeField, Range(1, 50)]//行数
private int row;
[SerializeField, Range(1, 50)]//列数
private int col;
[SerializeField]
private Direction direction = Direction.Horizontal;//方向
public delegate void UILoopCallBack(int index, GameObject go);
public UILoopCallBack onCreateItem;//创建子项回调
public UILoopCallBack onUpdateItem;//更新子项回调
private int createIndex = 0;
private List<RectTransform> showItems = new List<RectTransform>();
private List<RectTransform> poolItems = new List<RectTransform>();
private static readonly Vector2 InvalidPos = new Vector2(99999f, 99999f);//一个不可到达的位置
private RectTransform contentRect;
private RectTransform cacheRect
{
get
{
if (contentRect == null)
{
contentRect = GetComponent<RectTransform>();
contentRect.anchoredPosition = Vector2.zero;
contentRect.anchorMax = Vector2.up;
contentRect.anchorMin = Vector2.up;
contentRect.pivot = Vector2.up;
}
return contentRect;
}
}
private int itemCounts;
public int ItemCounts
{
get
{
return itemCounts;
}
set
{
itemCounts = value;
updateAll();
}
}
private void Awake()
{
if (cell.gameObject.activeSelf)
cell.gameObject.SetActive(false);
resetBounds();
}
void updateAll()
{
clearShowItems();
addShowItems();
resetBounds();
}
//移除之前列表中子项显示
void clearShowItems()
{
var count = showItems.Count;
if (count > 0)
{
for (var i = count - 1; i >= 0; i--)
{
showItems[i].position = InvalidPos;
poolItems.Add(showItems[i]);
}
showItems.Clear();
}
}
//生成子项
void addShowItems()
{
for (var i = 0; i < itemCounts; i++)
{
RectTransform rectItem = getItemFromPool();
rectItem.anchoredPosition = GetPosByIndex(i);
showItems.Add(rectItem);
if (onUpdateItem != null)
onUpdateItem(i, rectItem.gameObject);
}
}
private RectTransform createItem()
{
RectTransform item = GameObject.Instantiate(cell);//创建item,并重置坐标
item.SetParent(cacheRect, false);
item.anchorMax = Vector2.up;
item.anchorMin = Vector2.up;
item.pivot = new Vector2(0.5f, 0.5f);
item.anchoredPosition3D = Vector3.zero;
item.gameObject.SetActive(true);
if (onCreateItem != null)
onCreateItem(createIndex, item.gameObject);
createIndex++;
return item;
}
//获取子项
private RectTransform getItemFromPool()
{
if (poolItems.Count > 0)
{
RectTransform rect = poolItems[poolItems.Count - 1];
poolItems.RemoveAt(poolItems.Count - 1);
return rect;
}
return createItem();
}
private Vector2 GetPosByIndex(int index)
{
if (index < 0)
index = 0;
float x;
float y;
if (direction == Direction.Horizontal)
{
x = index / row;
y = index % row;
}
else
{
x = index % col;
y = index / col;
}
Vector2 nativeSize = cell.sizeDelta;
x = x * cellSize.x + nativeSize.x * 0.5f;
y = -(y * cellSize.y + nativeSize.y * 0.5f);
return new Vector2(x, y);
}
void resetBounds()
{
int m_row = row;
int m_col = col;
if (direction == Direction.Horizontal)
{
m_col = itemCounts / row;
if (itemCounts % row != 0)
m_col++;
m_col = Mathf.Max(m_col, col - 1);
}
else
{
m_row = itemCounts / col;
if (itemCounts % col != 0)
m_row++;
m_row = Mathf.Max(m_row, row - 1);
}
cacheRect.sizeDelta = new Vector2(m_col * cellSize.x - cell_gap.x, m_row * cellSize.y - cell_gap.y);
}
private Vector2 cellSize { get { return cell.sizeDelta + cell_gap; } }
}