unity实现一个列表组件,为scroll view中动态创建显示子项,实现思路和过程--unity学习笔记

在做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; } }
}

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值