【游戏开发实战】Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码

一、前言

点关注不迷路,持续输出Unity干货文章。

嗨,大家好,我是新发。
游戏开发中,经常需要用到列表显示,比如排行榜列表、邮件列表、好友列表等等,而当列表数据很多时,我们就要考虑使用循环复用列表了,比如循环复用使用10个item来显示100个数据。

二、原理

原理其实就是,列表向上滑动时,当item超过显示区域的上边界时,把item移动到列表底部,重复使用item并更新itemui显示,向下滑动同理,把超过显示区域底部的item复用到顶部。
为了方便大家理解,我成图,如下:
在这里插入图片描述

三、最终实现效果

本文Unity Demo工程我已上传到CodeChina,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityRecyclingListDemo
注,我使用的Unity版本:2020.2.7f1c1 (64-bit)
在这里插入图片描述

运行效果如下:
创建列表:
在这里插入图片描述
使用9item循环复用,可以显示大量的列表项。
在这里插入图片描述
清空列表:
在这里插入图片描述
添加行:
在这里插入图片描述
删除指定行:
在这里插入图片描述
移动指定行到列表的顶部、中间、底部:
在这里插入图片描述

四、具体使用

1、循环列表脚本:RecyclingListView

RecyclingListView:循环列表类,具体代码参见文章末尾。
在这里插入图片描述
主要接口和属性:

/// <summary>
/// 列表行数,赋值时,会执行列表重新计算
/// </summary>
public int RowCount

/// <summary>
/// 供外部调用,强制刷新整个列表,比如数据变化了,刷新一下列表
/// </summary>
public virtual void Refresh()

/// <summary>
/// 供外部调用,强制刷新整个列表的局部item
/// </summary>
/// <param name="rowStart">开始行</param>
/// <param name="count">数量</param>
public virtual void Refresh(int rowStart, int count)

/// <summary>
/// 供外部调用,强制刷新整个列表的某一个item
/// </summary>
public virtual void Refresh(RecyclingListViewItem item)

/// <summary>
/// 清空列表
/// </summary>
public virtual void Clear()

/// <summary>
/// 供外部调用,强制滚动列表,使某一行显示在列表中
/// </summary>
/// <param name="row">行号</param>
/// <param name="posType">目标行显示在列表的位置:顶部,中心,底部</param>
public virtual void ScrollToRow(int row, ScrollPosType posType)

RecyclingListView脚本需要挂在ScrollRect所在的节点上。
在这里插入图片描述
有三个参数:
Child Obj:列表项itemItem必须挂RecyclingListViewItem或它的子类脚本。
Row Padding:列表item之间的间隔。
Pre Alloc Height:预先分配列表高度,默认为0。

2、列表item脚本:RecyclingListViewItem

列表item基类,主要接口和属性:

/// <summary>
/// 循环列表
/// </summary>
public RecyclingListView ParentList
/// <summary>
/// 行号
/// </summary>
public int CurrentRow

public RectTransform RectTransform

/// <summary>
/// item更新事件响应函数
/// </summary>
public virtual void NotifyCurrentAssignment(RecyclingListView v, int row)

具体列表item需要继承RecyclingListViewItem,在子类中实现具体更新item逻辑,例:

using UnityEngine.UI;

public class TestChildItem : RecyclingListViewItem
{
    public Text titleText;
    public Text rowText;

    private TestChildData childData;
    public TestChildData ChildData
    {
        get { return childData; }
        set
        {
            childData = value;
            titleText.text = childData.Title;
            rowText.text = $"行号:{childData.Row}";
        }
    }
}


public struct TestChildData
{
    public string Title;
    public int Row;

    public TestChildData(string title, int row)
    {
        Title = title;
        Row = row;
    }
}

列表item挂上TestChildItem脚本。
在这里插入图片描述

3、测试脚本

做个简单的测试界面:
在这里插入图片描述
写个测试脚本TestPanel,测试一下循环列表,挂在TestPanel节点上,赋值对应的ui对象。
在这里插入图片描述
TestPanel代码如下:

using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
using UnityEngine.UI;

public class TestPanel : MonoBehaviour
{
    public RecyclingListView scrollList;
    /// <summary>
    /// 列表数据
    /// </summary>
    private List<TestChildData> data = new List<TestChildData>();

    public InputField createRowCntInput;
    public Button createListBtn;
    public Button clearListBtn;

    public InputField deleteItemInput;
    public Button deleteItemBtn;
    public Button addItemBtn;

    public InputField moveToRowInput;
    public Button moveToTopBtn;
    public Button moveToCenterBtn;
    public Button moveToBottomBtn;

    private void Start()
    {
        // 列表item更新回调
        scrollList.ItemCallback = PopulateItem;

        // 创建列表
        createListBtn.onClick.AddListener(CreateList);

        // 清空列表
        clearListBtn.onClick.AddListener(ClearList);


        // 删除某一行
        deleteItemBtn.onClick.AddListener(DeleteItem);

        // 添加行
        addItemBtn.onClick.AddListener(AddItem);

        // 将目标行移动到列表的顶部、中心、底部
        moveToTopBtn.onClick.AddListener(() => 
        {
            MoveToRow(RecyclingListView.ScrollPosType.Top);
        });

        moveToCenterBtn.onClick.AddListener(() =>
        {
            MoveToRow(RecyclingListView.ScrollPosType.Center);
        });

        moveToBottomBtn.onClick.AddListener(() =>
        {
            MoveToRow(RecyclingListView.ScrollPosType.Bottom);
        });
    }

    /// <summary>
    /// item更新回调
    /// </summary>
    /// <param name="item">复用的item对象</param>
    /// <param name="rowIndex">行号</param>
    private void PopulateItem(RecyclingListViewItem item, int rowIndex)
    {
        var child = item as TestChildItem;
        child.ChildData = data[rowIndex];
    }

    private void CreateList()
    {
        if (string.IsNullOrEmpty(createRowCntInput.text))
        {
            Debug.LogError("请输入行数");
            return;
        }
        var rowCnt = int.Parse(createRowCntInput.text);

        data.Clear();
        // 模拟数据
        string[] randomTitles = new[] {
            "黄沙百战穿金甲,不破楼兰终不还",
            "且将新火试新茶,诗酒趁年华",
            "苟利国家生死以,岂因祸福避趋之",
            "枫叶经霜艳,梅花透雪香",
            "夏虫不可语于冰",
            "落花无言,人淡如菊",
            "宠辱不惊,闲看庭前花开花落;去留无意,漫随天外云卷云舒",
            "衣带渐宽终不悔,为伊消得人憔悴",
            "从善如登,从恶如崩",
            "欲穷千里目,更上一层楼",
            "草木本无意,荣枯自有时",
            "纸上得来终觉浅,绝知此事要躬行",
            "不是一番梅彻骨,怎得梅花扑鼻香",
            "青青子衿,悠悠我心",
            "瓜田不纳履,李下不正冠"
        };
        for (int i = 0; i < rowCnt; ++i)
        {
            data.Add(new TestChildData(randomTitles[Random.Range(0, randomTitles.Length)], i));
        }

        // 设置数据,此时列表会执行更新
        scrollList.RowCount = data.Count;
    }

    private void ClearList()
    {
        data.Clear();
        scrollList.Clear();
    }

    private void DeleteItem()
    {
        if (string.IsNullOrEmpty(deleteItemInput.text))
        {
            Debug.LogError("请输入行号");
            return;
        }
        var rowIndex = int.Parse(deleteItemInput.text);
        data.RemoveAll(item => (item.Row == rowIndex));

        scrollList.RowCount = data.Count;
    }

    private void AddItem()
    {
        data.Add(new TestChildData("我是新增的行", data.Count));
        scrollList.RowCount = data.Count;
    }

    private void MoveToRow(RecyclingListView.ScrollPosType posType)
    {
        if (string.IsNullOrEmpty(moveToRowInput.text))
        {
            Debug.LogError("请输入行号");
            return;
        }
        var rowIndex = int.Parse(moveToRowInput.text);
        scrollList.ScrollToRow(rowIndex, posType);
    }
}

测试效果见文章最上面。
完毕。
喜欢Unity的同学,不要忘记点击关注,如果有什么Unity相关的技术难题,也欢迎留言或私信~

五、附录

脚本注释我写的比较清楚了,大家应该可以看懂。如果不懂怎么用的,可以下载我的Demo工程进行学习。

1、RecyclingListView.cs
using System;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 循环复用列表
/// </summary>
[RequireComponent(typeof(ScrollRect))]
public class RecyclingListView : MonoBehaviour
{
    [Tooltip("子节点物体")]
    public RecyclingListViewItem ChildObj;
    [Tooltip("行间隔")]
    public float RowPadding = 15f;
    [Tooltip("事先预留的最小列表高度")]
    public float PreAllocHeight = 0;

    public enum ScrollPosType
    {
        Top,
        Center,
        Bottom,
    }


    public float VerticalNormalizedPosition
    {
        get => scrollRect.verticalNormalizedPosition;
        set => scrollRect.verticalNormalizedPosition = value;
    }


    /// <summary>
    /// 列表行数
    /// </summary>
    protected int rowCount;

    /// <summary>
    /// 列表行数,赋值时,会执行列表重新计算
    /// </summary>
    public int RowCount
    {
        get => rowCount;
        set
        {
            if (rowCount != value)
            {
                rowCount = value;
                // 先禁用滚动变化
                ignoreScrollChange = true;
                // 更新高度
                UpdateContentHeight();
                // 重新启用滚动变化
                ignoreScrollChange = false;
                // 重新计算item
                ReorganiseContent(true);
            }
        }
    }

    /// <summary>
    /// item更新回调函数委托
    /// </summary>
    /// <param name="item">子节点对象</param>
    /// <param name="rowIndex">行数</param>
    public delegate void ItemDelegate(RecyclingListViewItem item, int rowIndex);

    /// <summary>
    /// item更新回调函数委托
    /// </summary>
    public ItemDelegate ItemCallback;

    protected ScrollRect scrollRect;
    /// <summary>
    /// 复用的item数组
    /// </summary>
    protected RecyclingListViewItem[] childItems;

    /// <summary>
    /// 循环列表中,第一个item的索引,最开始每个item都有一个原始索引,最顶部的item的原始索引就是childBufferStart
    /// 由于列表是循环复用的,所以往下滑动时,childBufferStart会从0开始到n,然后又从0开始,以此往复
    /// 如果是往上滑动,则是从0到-n,再从0开始,以此往复
    /// </summary>
    protected int childBufferStart = 0;
    /// <summary>
    /// 列表中最顶部的item的真实数据索引,比如有一百条数据,复用10个item,当前最顶部是第60条数据,那么sourceDataRowStart就是59(注意索引从0开始)
    /// </summary>
    protected int sourceDataRowStart;

    protected bool ignoreScrollChange = false;
    protected float previousBuildHeight = 0;
    protected const int rowsAboveBelow = 1;

    protected virtual void Awake()
    {
        scrollRect = GetComponent<ScrollRect>();
        ChildObj.gameObject.SetActive(false);
    }


    protected virtual void OnEnable()
    {
        scrollRect.onValueChanged.AddListener(OnScrollChanged);
        ignoreScrollChange = false;
    }

    protected virtual void OnDisable()
    {
        scrollRect.onValueChanged.RemoveListener(OnScrollChanged);
    }


    /// <summary>
    /// 供外部调用,强制刷新整个列表,比如数据变化了,刷新一下列表
    /// </summary>
    public virtual void Refresh()
    {
        ReorganiseContent(true);
    }

    /// <summary>
    /// 供外部调用,强制刷新整个列表的局部item
    /// </summary>
    /// <param name="rowStart">开始行</param>
    /// <param name="count">数量</param>
    public virtual void Refresh(int rowStart, int count)
    {
        int sourceDataLimit = sourceDataRowStart + childItems.Length;
        for (int i = 0; i < count; ++i)
        {
            int row = rowStart + i;
            if (row < sourceDataRowStart || row >= sourceDataLimit)
                continue;

            int bufIdx = WrapChildIndex(childBufferStart + row - sourceDataRowStart);
            if (childItems[bufIdx] != null)
            {
                UpdateChild(childItems[bufIdx], row);
            }
        }
    }

    /// <summary>
    /// 供外部调用,强制刷新整个列表的某一个item
    /// </summary>
    public virtual void Refresh(RecyclingListViewItem item)
    {

        for (int i = 0; i < childItems.Length; ++i)
        {
            int idx = WrapChildIndex(childBufferStart + i);
            if (childItems[idx] != null && childItems[idx] == item)
            {
                UpdateChild(childItems[i], sourceDataRowStart + i);
                break;
            }
        }
    }

    /// <summary>
    /// 清空列表
    /// </summary>
    public virtual void Clear()
    {
        RowCount = 0;
    }


    /// <summary>
    /// 供外部调用,强制滚动列表,使某一行显示在列表中
    /// </summary>
    /// <param name="row">行号</param>
    /// <param name="posType">目标行显示在列表的位置:顶部,中心,底部</param>
    public virtual void ScrollToRow(int row, ScrollPosType posType)
    {
        scrollRect.verticalNormalizedPosition = GetRowScrollPosition(row, posType);
    }

    /// <summary>
    /// 获得归一化的滚动位置,该位置将给定的行在视图中居中
    /// </summary>
    /// <param name="row">行号</param>
    /// <returns></returns>
    public float GetRowScrollPosition(int row, ScrollPosType posType)
    {
        // 视图高
        float vpHeight = ViewportHeight();
        float rowHeight = RowHeight();
        // 将目标行滚动到列表目标位置时,列表顶部的位置
        float vpTop = 0;
        switch (posType)
        {
            case ScrollPosType.Top:
                {
                    vpTop = row * rowHeight;
                }
                break;
            case ScrollPosType.Center:
                {
                    // 目标行的中心位置与列表顶部的距离
                    float rowCentre = (row + 0.5f) * rowHeight;
                    // 视口中心位置
                    float halfVpHeight = vpHeight * 0.5f;

                    vpTop = Mathf.Max(0, rowCentre - halfVpHeight);
                }
                break;
            case ScrollPosType.Bottom:
                {
                    vpTop = (row+1) * rowHeight - vpHeight;
                }
                break;
        }


        // 滚动后,列表底部的位置
        float vpBottom = vpTop + vpHeight;
        // 列表内容总高度
        float contentHeight = scrollRect.content.sizeDelta.y;
        // 如果滚动后,列表底部的位置已经超过了列表总高度,则调整列表顶部的位置
        if (vpBottom > contentHeight)
            vpTop = Mathf.Max(0, vpTop - (vpBottom - contentHeight));

        // 反插值,计算两个值之间的Lerp参数。也就是value在from和to之间的比例值
        return Mathf.InverseLerp(contentHeight - vpHeight, 0, vpTop);
    }

    /// <summary>
    /// 根据行号获取复用的item对象
    /// </summary>
    /// <param name="row">行号</param>
    protected RecyclingListViewItem GetRowItem(int row)
    {
        if (childItems != null &&
            row >= sourceDataRowStart && row < sourceDataRowStart + childItems.Length &&
            row < rowCount)
        {
            // 注意这里要根据行号计算复用的item原始索引
            return childItems[WrapChildIndex(childBufferStart + row - sourceDataRowStart)];
        }

        return null;
    }

    protected virtual bool CheckChildItems()
    {
        // 列表视口高度
        float vpHeight = ViewportHeight();
        float buildHeight = Mathf.Max(vpHeight, PreAllocHeight);
        bool rebuild = childItems == null || buildHeight > previousBuildHeight;
        if (rebuild)
        {

            int childCount = Mathf.RoundToInt(0.5f + buildHeight / RowHeight());
            childCount += rowsAboveBelow * 2;

            if (childItems == null)
                childItems = new RecyclingListViewItem[childCount];
            else if (childCount > childItems.Length)
                Array.Resize(ref childItems, childCount);

            // 创建item
            for (int i = 0; i < childItems.Length; ++i)
            {
                if (childItems[i] == null)
                {
                    var item = Instantiate(ChildObj);
                    childItems[i] = item;
                }
                childItems[i].RectTransform.SetParent(scrollRect.content, false);
                childItems[i].gameObject.SetActive(false);
            }

            previousBuildHeight = buildHeight;
        }

        return rebuild;
    }


    /// <summary>
    /// 列表滚动时,会回调此函数
    /// </summary>
    /// <param name="normalisedPos">归一化的位置</param>
    protected virtual void OnScrollChanged(Vector2 normalisedPos)
    {
        if (!ignoreScrollChange)
        {
            ReorganiseContent(false);
        }
    }

    /// <summary>
    /// 重新计算列表内容
    /// </summary>
    /// <param name="clearContents">是否要清空列表重新计算</param>
    protected virtual void ReorganiseContent(bool clearContents)
    {

        if (clearContents)
        {
            scrollRect.StopMovement();
            scrollRect.verticalNormalizedPosition = 1;
        }

        bool childrenChanged = CheckChildItems();
        // 是否要更新整个列表
        bool populateAll = childrenChanged || clearContents;


        float ymin = scrollRect.content.localPosition.y;

        // 第一个可见item的索引
        int firstVisibleIndex = (int)(ymin / RowHeight());


        int newRowStart = firstVisibleIndex - rowsAboveBelow;

        // 滚动变化量
        int diff = newRowStart - sourceDataRowStart;
        if (populateAll || Mathf.Abs(diff) >= childItems.Length)
        {

            sourceDataRowStart = newRowStart;
            childBufferStart = 0;
            int rowIdx = newRowStart;
            foreach (var item in childItems)
            {
                UpdateChild(item, rowIdx++);
            }

        }
        else if (diff != 0)
        {
            int newBufferStart = (childBufferStart + diff) % childItems.Length;

            if (diff < 0)
            {
                // 向前滑动
                for (int i = 1; i <= -diff; ++i)
                {
                    // 得到复用item的索引
                    int wrapIndex = WrapChildIndex(childBufferStart - i);
                    int rowIdx = sourceDataRowStart - i;
                    UpdateChild(childItems[wrapIndex], rowIdx);
                }
            }
            else
            {
                // 向后滑动
                int prevLastBufIdx = childBufferStart + childItems.Length - 1;
                int prevLastRowIdx = sourceDataRowStart + childItems.Length - 1;
                for (int i = 1; i <= diff; ++i)
                {
                    int wrapIndex = WrapChildIndex(prevLastBufIdx + i);
                    int rowIdx = prevLastRowIdx + i;
                    UpdateChild(childItems[wrapIndex], rowIdx);
                }
            }

            sourceDataRowStart = newRowStart;

            childBufferStart = newBufferStart;
        }
    }

    private int WrapChildIndex(int idx)
    {
        while (idx < 0)
            idx += childItems.Length;

        return idx % childItems.Length;
    }

    /// <summary>
    /// 获取一行的高度,注意要加上RowPadding
    /// </summary>
    private float RowHeight()
    {
        return RowPadding + ChildObj.RectTransform.rect.height;
    }

    /// <summary>
    /// 获取列表视口的高度
    /// </summary>
    private float ViewportHeight()
    {
        return scrollRect.viewport.rect.height;
    }

    protected virtual void UpdateChild(RecyclingListViewItem child, int rowIdx)
    {
        if (rowIdx < 0 || rowIdx >= rowCount)
        {
            child.gameObject.SetActive(false);
        }
        else
        {
            if (ItemCallback == null)
            {
                Debug.Log("RecyclingListView is missing an ItemCallback, cannot function", this);
                return;
            }

            // 移动到正确的位置
            var childRect = ChildObj.RectTransform.rect;
            Vector2 pivot = ChildObj.RectTransform.pivot;
            float ytoppos = RowHeight() * rowIdx;
            float ypos = ytoppos + (1f - pivot.y) * childRect.height;
            float xpos = 0 + pivot.x * childRect.width;
            child.RectTransform.anchoredPosition = new Vector2(xpos, -ypos);
            child.NotifyCurrentAssignment(this, rowIdx);

            // 更新数据
            ItemCallback(child, rowIdx);

            child.gameObject.SetActive(true);
        }
    }

    /// <summary>
    /// 更新content的高度
    /// </summary>
    protected virtual void UpdateContentHeight()
    {
        // 列表高度
        float height = ChildObj.RectTransform.rect.height * rowCount + (rowCount - 1) * RowPadding;
        // 更新content的高度
        var sz = scrollRect.content.sizeDelta;
        scrollRect.content.sizeDelta = new Vector2(sz.x, height);
    }

    protected virtual void DisableAllChildren()
    {
        if (childItems != null)
        {
            for (int i = 0; i < childItems.Length; ++i)
            {
                childItems[i].gameObject.SetActive(false);
            }
        }
    }
}
2、RecyclingListViewItem.cs
using UnityEngine;

/// <summary>
/// 列表item,你自己写的列表item需要继承该类
/// </summary>
[RequireComponent(typeof(RectTransform))]
public class RecyclingListViewItem : MonoBehaviour
{

    private RecyclingListView parentList;

    /// <summary>
    /// 循环列表
    /// </summary>
    public RecyclingListView ParentList
    {
        get => parentList;
    }

    private int currentRow;
    /// <summary>
    /// 行号
    /// </summary>
    public int CurrentRow
    {
        get => currentRow;
    }

    private RectTransform rectTransform;
    public RectTransform RectTransform
    {
        get
        {
            if (rectTransform == null)
                rectTransform = GetComponent<RectTransform>();
            return rectTransform;
        }
    }

    private void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
    }

    /// <summary>
    /// item更新事件响应函数
    /// </summary>
    public virtual void NotifyCurrentAssignment(RecyclingListView v, int row)
    {
        parentList = v;
        currentRow = row;
    }
}

  • 68
    点赞
  • 186
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
### 回答1: Unity UGUI 列表是一种常见的 UI 控件,用于展示类别、选项或者数据。它通常由一个滚动视图组成,包一个或多个列表项,每个列表项包显示内容、图标和按钮。 在 Unity 中,实现列表通常需要以下步骤: 1. 创建一个滚动区域并设置相关参数,如大小、位置、滚动方向等等。 2. 在滚动区域内创建列表项,通常使用一个 prefab 来实现。 3. 让列表项拥有响应鼠标事件的组件,通常是按钮或 Toggle 组件。 4. 定义列表项的数据结构,如包名称、描述、图标等属性。 5. 将数据与列表项绑定,使得列表项能够根据数据源的变化自动更新。 6. 定义列表项的样式,如字体、颜色、对齐方式等等。 7. 编写列表项的逻辑代码,处理用户点击触发的事件。 总之,Unity UGUI 列表是一种非常常见和实用的 UI 控件,可以用于许多不同的场景,如游戏菜单、设置界面、排行榜等等。有了列表的支持,用户可以更加便捷地浏览和操作大量的信息。 ### 回答2: Unity UGUI列表是一个UI组件,它允许开发者在游戏设计中创建各种不同类型的列表。在列表中,开发者可以显示一系列项目,用于展示各种信息和元素,例如角色列表、任务列表、排行榜等。 这个组件提供了一些控制选项,例如滚动条、滑动速度、自动布局等。开发者可以使用这些选项来控制列表的行为,使其更接近预期的效果。列表还可以支持不同的布局方式,例如网格布局、表格布局、平铺布局等,以便显示不同格式的项目。 Unity UGUI列表还允许开发者自定义项目的外观和交互。每个项目都可以选择不同的背景图片、字体、颜色和图标等。开发者还可以在每个项目上定义按钮事件或其他交互事件,例如打开一个菜单、播放动态内容等。 总的来说,Unity UGUI列表是一个很有用的UI组件,它可以简化游戏设计中的列表创建过程,并提供了灵活的控制和交互选项。开发者可以利用它来创建丰富的游戏UI,为玩家提供更好的交互和用户体验。 ### 回答3: Unity UGUI 列表Unity 引擎中的一个 UI 元素,用于显示和控制列表中的项目。列表通常用于显示大量的数据,如图像、文本或按钮。Unity UGUI 中的列表组件支持选择、滚动和无限滚动功能。列表组件可以通过在游戏对象中添加 Scroll View、Viewport 和 Content 三个组件来实现。Scroll View 是列表的外壳,Viewport 是显示列表的区域,Content 是列表中的项目。在 Content 中,通过使用 Layout Group 和 Content Size Fitter 来控制列表中的项目排列和大小。 Unity UGUI 列表可以和数据绑定一起使用,通过数据绑定可以动态地更新列表中的项目。例如,在游戏中选择一种角色后,可以通过数据绑定将角色属性动态地绑定到列表中的项目,从而更新列表中的显示内容。列表组件还支持使用导航组件来快速地在列表中导航。在导航组件中,可以使用键盘或鼠标来快速地选择列表中的项目。 Unity UGUI 列表游戏开发中非常重要的一个元素,它可以用来创建各种类型的列表,如任务列表、道具列表等。在使用列表组件时,需要仔细地考虑列表的设计和布局,以便用户可以轻松地使用列表。使用 Unity UGUI 列表可以使游戏界面变得更加直观和易于操作,增强游戏的可玩性和用户体验。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林新发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值