七种 Unity UGUI 列表刷新模式

下面详细讲解七种 Unity UGUI 列表刷新模式,并为每一种提供可直接参考的代码示例。所有示例基于 UnityEngine.UI 和 UnityEngine 命名空间,假设已引用相关库。

实际上一般就第一种和第三种用的最多

1. 全量刷新(Full Refresh)

原理

销毁 content 下的所有子物体,然后根据当前数据源的数量重新创建对应数量的 UI 元素,并通过回调绑定数据。

优点

  • 实现简单,逻辑清晰。
  • 确保 UI 与数据完全同步,无状态残留。

缺点

  • 频繁的销毁/创建会产生大量 GC,性能较差。
  • 不适用于长列表(>100)或频繁刷新场景。

适用场景

  • 列表项很少(<30)。
  • 内容完全替换且刷新频率低(如设置面板、角色选择界面)。

代码示例

csharp

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

public static class ListRefreshFull
{
    /// <summary>
    /// 全量刷新列表
    /// </summary>
    public static List<T> RefreshFull<T>(this RectTransform content, List<string> dataList, 
                                          GameObject itemPrefab, System.Action<T, string> onRefresh) 
                                          where T : Component
    {
        // 1. 销毁所有子物体
        foreach (Transform child in content)
            Object.Destroy(child.gameObject);

        List<T> items = new List<T>();

        // 2. 根据数据数量重新创建
        foreach (string data in dataList)
        {
            GameObject go = Object.Instantiate(itemPrefab, content);
            T item = go.GetComponent<T>();
            onRefresh?.Invoke(item, data);
            items.Add(item);
        }

        // 3. 强制重建布局
        LayoutRebuilder.ForceRebuildLayoutImmediate(content);
        return items;
    }
}

// 使用示例
public class DemoFullRefresh : MonoBehaviour
{
    public RectTransform content;
    public GameObject itemPrefab;

    void Start()
    {
        List<string> players = new List<string> { "Alice", "Bob", "Charlie" };
        content.RefreshFull(players, itemPrefab, (item, name) =>
        {
            item.GetComponent<Text>().text = name;
        });
    }
}

2. 增量刷新(Incremental Update)

原理

保持现有子物体数量不变,遍历所有子物体,用新数据逐个更新其显示内容。如果数据数量与现有子物体数量不一致,则通过增加或删除子物体来匹配。

优点

  • 没有创建/销毁开销(除数量调整外)。
  • 适合数据频繁变化但列表结构稳定(增删少)的场景。

缺点

  • 需要处理数量不一致的逻辑。
  • 如果数据完全打乱(例如排序后),可能需要重新排列子物体顺序。

适用场景

  • 列表长度基本固定,只是内容数值变化(如实时排行榜、计分板)。

代码示例

csharp

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

public static class ListRefreshIncremental
{
    public static void RefreshIncremental<T>(this RectTransform content, List<string> newData, 
                                              GameObject itemPrefab, System.Action<T, string> onRefresh) 
                                              where T : Component
    {
        int currentCount = content.childCount;
        int newCount = newData.Count;

        // 1. 数量调整:多了就删,少了就加
        if (currentCount > newCount)
        {
            for (int i = newCount; i < currentCount; i++)
                Object.Destroy(content.GetChild(i).gameObject);
        }
        else if (currentCount < newCount)
        {
            for (int i = currentCount; i < newCount; i++)
                Object.Instantiate(itemPrefab, content);
        }

        // 2. 更新所有现有元素
        for (int i = 0; i < newCount; i++)
        {
            Transform child = content.GetChild(i);
            T item = child.GetComponent<T>();
            onRefresh?.Invoke(item, newData[i]);
        }

        LayoutRebuilder.ForceRebuildLayoutImmediate(content);
    }
}

// 使用示例
public class DemoIncremental : MonoBehaviour
{
    public RectTransform content;
    public GameObject itemPrefab;
    private List<string> data = new List<string> { "A", "B", "C" };

    void Update()
    {
        // 模拟数据变化
        for (int i = 0; i < data.Count; i++)
            data[i] = $"Value {Random.Range(0, 100)}";
        content.RefreshIncremental(data, itemPrefab, (Text txt, string val) => txt.text = val);
    }
}

3. 对象池复用刷新(Pooling)

原理

预先创建一批 UI 元素放入池中(Stack 或 Queue)。刷新时从池中取出所需数量的元素激活并放入 content;多余元素回收到池中(停用并移出父级)。完全避免 Instantiate 和 Destroy 的 GC。

优点

  • 极大减少 GC,性能优秀。
  • 适用于中等长度(50~500)且频繁整体刷新的列表。

缺点

  • 需要额外维护对象池逻辑。
  • 池的初始预热需要时间。

适用场景

  • 聊天记录、商品列表、任务列表等频繁完全替换内容的界面。

代码示例

csharp

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

public class UIObjectPool<T> where T : Component
{
    private GameObject prefab;
    private Transform parent;
    private Stack<T> pool = new Stack<T>();

    public UIObjectPool(GameObject prefab, Transform parent, int preloadCount = 5)
    {
        this.prefab = prefab;
        this.parent = parent;
        for (int i = 0; i < preloadCount; i++)
            CreateNew();
    }

    private T CreateNew()
    {
        GameObject go = Object.Instantiate(prefab, parent);
        go.SetActive(false);
        T comp = go.GetComponent<T>();
        pool.Push(comp);
        return comp;
    }

    public T Get()
    {
        if (pool.Count == 0) CreateNew();
        T item = pool.Pop();
        item.gameObject.SetActive(true);
        return item;
    }

    public void Return(T item)
    {
        item.gameObject.SetActive(false);
        item.transform.SetParent(parent);
        pool.Push(item);
    }
}

public static class ListRefreshPooling
{
    public static List<T> RefreshWithPool<T>(this RectTransform content, List<string> dataList,
                                             UIObjectPool<T> pool, System.Action<T, string> onRefresh) 
                                             where T : Component
    {
        // 1. 将所有当前子物体回收到池中
        foreach (Transform child in content)
        {
            T item = child.GetComponent<T>();
            if (item != null) pool.Return(item);
        }

        // 2. 从池中取出新数据所需的元素,并重新设置父级为 content
        List<T> activeItems = new List<T>();
        foreach (string data in dataList)
        {
            T item = pool.Get();
            item.transform.SetParent(content);
            onRefresh?.Invoke(item, data);
            activeItems.Add(item);
        }

        LayoutRebuilder.ForceRebuildLayoutImmediate(content);
        return activeItems;
    }
}

// 使用示例
public class DemoPooling : MonoBehaviour
{
    public RectTransform content;
    public GameObject itemPrefab;
    private UIObjectPool<Text> pool;

    void Start()
    {
        pool = new UIObjectPool<Text>(itemPrefab, content, 10);
    }

    void RefreshList(List<string> newData)
    {
        content.RefreshWithPool(newData, pool, (txt, val) => txt.text = val);
    }
}

4. 虚拟滚动 / 动态复用(Virtualization)

原理

只创建足以填满视口(Viewport)的 UI 元素(例如 10~20 个)。监听 ScrollRect 的滚动事件,根据滚动偏移量计算出当前应该显示的数据范围,然后移动现有元素的位置并重新绑定数据,实现无限滚动。

优点

  • 内存占用固定,支持百万级数据。
  • 滚动流畅。

缺点

  • 实现复杂,需要精确计算元素位置、尺寸以及复用逻辑。
  • 要求所有元素尺寸固定或能提前计算。

适用场景

  • 好友列表、文件浏览器、无尽下拉加载。

简化代码示例(固定高度、单向滚动)

csharp

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

public class VirtualScrollList : MonoBehaviour
{
    public ScrollRect scrollRect;
    public RectTransform content;
    public GameObject itemPrefab;
    public float itemHeight = 50f;

    private List<string> data = new List<string>();     // 假设有 10000 条数据
    private List<RectTransform> items = new List<RectTransform>();
    private int currentTopIndex = 0;

    void Start()
    {
        // 预先创建足够填满视口的元素(视口高度 / itemHeight + 2)
        int visibleCount = Mathf.CeilToInt(scrollRect.viewport.rect.height / itemHeight) + 2;
        for (int i = 0; i < visibleCount; i++)
        {
            GameObject go = Instantiate(itemPrefab, content);
            RectTransform rect = go.GetComponent<RectTransform>();
            items.Add(rect);
        }

        scrollRect.onValueChanged.AddListener(OnScroll);
        UpdateContentHeight();
        RefreshVisibleItems(0);
    }

    void UpdateContentHeight()
    {
        content.sizeDelta = new Vector2(content.sizeDelta.x, data.Count * itemHeight);
    }

    void OnScroll(Vector2 _)
    {
        float scrollPos = content.anchoredPosition.y;
        int newTopIndex = Mathf.FloorToInt(scrollPos / itemHeight);
        newTopIndex = Mathf.Clamp(newTopIndex, 0, data.Count - items.Count);
        if (newTopIndex != currentTopIndex)
        {
            currentTopIndex = newTopIndex;
            RefreshVisibleItems(currentTopIndex);
        }
    }

    void RefreshVisibleItems(int startIndex)
    {
        for (int i = 0; i < items.Count; i++)
        {
            int dataIndex = startIndex + i;
            if (dataIndex < data.Count)
            {
                items[i].gameObject.SetActive(true);
                items[i].anchoredPosition = new Vector2(0, dataIndex * itemHeight);
                items[i].GetComponent<Text>().text = data[dataIndex];
            }
            else
            {
                items[i].gameObject.SetActive(false);
            }
        }
    }
}

5. 数据绑定自动刷新(Data Binding / MVVM)

原理

为数据模型实现 INotifyPropertyChanged 或使用响应式属性(如 UniRx 的 ReactiveProperty)。UI 元素订阅数据变化事件,当属性值改变时自动更新自身显示。列表级的变化(增删元素)通常结合 ObservableCollection 触发整体刷新。

优点

  • 完全解耦数据和视图,无需手动调用刷新方法。
  • 细粒度更新,性能好。

缺点

  • 需要引入响应式库(如 UniRx)或自己实现事件系统。
  • 列表整体替换时仍需配合其他模式。

适用场景

  • 复杂表单、实时监控面板、高度交互的 UI。

代码示例(使用 UniRx)

csharp

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using System.Collections.Generic;

// 数据模型
public class PlayerModel
{
    public ReactiveProperty<string> Name = new ReactiveProperty<string>();
    public ReactiveProperty<int> Level = new ReactiveProperty<int>();
}

// UI 绑定组件
public class PlayerView : MonoBehaviour
{
    public Text nameText;
    public Text levelText;

    public void Bind(PlayerModel model)
    {
        model.Name.SubscribeToText(nameText);
        model.Level.Subscribe(v => levelText.text = $"Lv.{v}");
    }
}

// 列表管理
public class DataBindingList : MonoBehaviour
{
    public RectTransform content;
    public GameObject itemPrefab;
    private List<PlayerModel> players = new List<PlayerModel>();
    private List<PlayerView> views = new List<PlayerView>();

    void Start()
    {
        // 创建初始数据
        for (int i = 0; i < 10; i++)
        {
            var model = new PlayerModel { Name = { Value = $"Player{i}" }, Level = { Value = i } };
            players.Add(model);
            var go = Instantiate(itemPrefab, content);
            var view = go.GetComponent<PlayerView>();
            view.Bind(model);
            views.Add(view);
        }

        // 模拟数据变化(UI 自动刷新)
        Observable.Interval(System.TimeSpan.FromSeconds(2)).Subscribe(_ =>
        {
            foreach (var p in players)
                p.Level.Value++;
        });
    }
}

6. 局部刷新(Partial Refresh)

原理

只更新列表中特定索引或特定条件的元素。需要维护一个从数据索引到 UI 元素的映射(如 Dictionary<int, UIElement>),或者通过 content.GetChild(index) 获取。

优点

  • 性能最佳,无 GC。
  • 实现简单。

缺点

  • 需要维护索引映射,且仅适合少量元素变化。
  • 对大批量变化不友好。

适用场景

  • 点赞、选中状态切换、某个玩家等级提升等单点变化。

代码示例

csharp

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

public class PartialRefreshDemo : MonoBehaviour
{
    public RectTransform content;
    public GameObject itemPrefab;
    private List<Text> itemTexts = new List<Text>();   // 维护映射
    private List<string> data = new List<string>();

    void Start()
    {
        // 初始化全量刷新
        data = new List<string> { "Apple", "Banana", "Cherry", "Date" };
        foreach (var d in data)
        {
            var go = Instantiate(itemPrefab, content);
            var txt = go.GetComponent<Text>();
            txt.text = d;
            itemTexts.Add(txt);
        }
    }

    public void UpdateItem(int index, string newValue)
    {
        if (index >= 0 && index < itemTexts.Count)
        {
            data[index] = newValue;
            itemTexts[index].text = newValue;
        }
    }

    // 例如点击按钮修改第2项
    public void OnButtonClick()
    {
        UpdateItem(1, "Blueberry");
    }
}

7. 混合模式(Hybrid)

原理

根据场景灵活组合多种模式。例如:使用对象池管理元素创建/销毁,同时采用增量刷新更新内容;或者虚拟滚动内部使用对象池复用可视元素;甚至结合数据绑定实现局部自动更新。

优点

  • 能够应对复杂多变的需求,平衡性能和开发成本。

缺点

  • 实现较复杂,需要仔细设计。

适用场景

  • 大型项目中的通用列表组件(如聊天 + 商品 + 排行榜共用一套高性能列表)。

代码示例(对象池 + 增量刷新)

csharp

// 扩展方法:先保证元素数量与数据匹配(对象池增删),再增量更新所有元素
public static void RefreshHybrid<T>(this RectTransform content, List<string> newData,
                                    UIObjectPool<T> pool, System.Action<T, string> onRefresh) 
                                    where T : Component
{
    int currentCount = content.childCount;
    int targetCount = newData.Count;

    // 1. 使用对象池调整数量
    if (currentCount > targetCount)
    {
        for (int i = targetCount; i < currentCount; i++)
        {
            T item = content.GetChild(i).GetComponent<T>();
            pool.Return(item);
        }
    }
    else if (currentCount < targetCount)
    {
        for (int i = currentCount; i < targetCount; i++)
        {
            T item = pool.Get();
            item.transform.SetParent(content);
        }
    }

    // 2. 增量更新所有现有元素
    for (int i = 0; i < targetCount; i++)
    {
        T item = content.GetChild(i).GetComponent<T>();
        onRefresh?.Invoke(item, newData[i]);
    }

    LayoutRebuilder.ForceRebuildLayoutImmediate(content);
}

总结

全量刷新

Destroy + Instantiate

<30

增量刷新

调整数量 + 遍历更新

良好

<200

对象池

Get / Return

优秀

50~500

虚拟滚动

动态复用可视元素

极佳

任意(>1000)

数据绑定

订阅事件自动更新

优秀

任意(需配合)

局部刷新

直接修改特定元素

最佳

单点变化

混合模式

按需组合

可调节

中~高

任意

根据项目具体需求(数据量、刷新频率、开发周期)选择合适的模式,才能写出高效又易于维护的 Unity UI 列表。

全部评论

相关推荐

03-31 21:47
东南大学 C++
彭于晏前来求offe...:吓晕了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务