Unity的ScrollView滚动视图复用

发现问题

在游戏开发中有一个常见的需求,就是需要在屏幕显示多个(多达上百)显示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的组件中完成这个功能。

解决前两个问题

框架代码

首先,对于前两个问题,我们来做一个简单的自定义滚动视图(先不处理复用的逻辑)。

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

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。

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代码:

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

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。

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。

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;
}
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脚本的显示对象,如下图所示。

最后

其中还有更多的细节,就未能一一提及。

当然还有更多有待优化的逻辑,需要大家来指出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/739973.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nest系列 - 4. 连接Mysql数据库以及typeOrm介绍

前面我们使用nest g res xxx 自动生成CRUD的代码&#xff0c;不仅简单&#xff0c;而且只能在本地玩。今天我们就来看nest 如何连接数据库&#xff0c;数据库有很多种&#xff0c;我们今天来看连接最常用mysql 数据库&#xff0c;并且使用typeOrm 进行数据库操作 mysql 安装 …

原装GUVCL-T21GH 韩国Genicom紫外线传感器光电二极管原厂代理商

深圳市宏南科技有限公司是韩国GenUV公司的原厂代理商&#xff0c;所售紫外线传感器均来自于原始生产厂商直接供货&#xff0c;非第三方转售。 韩国GENICOM 紫外线传感器 GUVCL-T21GH 特征&#xff1a; 单供电电压工作 电压输出 高灵敏度和良好的日盲性 尺寸小巧紧凑 韩国GENIC…

如何确保每颗螺丝都是合格品质

螺丝&#xff0c;一种用来连接和固定物体的金属件&#xff0c;通常是长有螺纹的金属棒。螺丝有不同种类和尺寸&#xff0c;常见的用途包括组装家具、机械设备和其他结构。连接和固定物体&#xff0c;通过螺丝的螺纹结构&#xff0c;将两个或多个物体牢固地连接在一起。提供调节…

研究上百个小时,高手总结了这份 DALL-E 3 人物连续性公式(上)

上篇 Dall-E 3 讲了常见的 20 个公式&#xff0c;今天单独来讲一下人物连续性公式&#xff0c;这个公式来自 AshutoshShrivastava。 上篇回顾&#xff1a; 效果超好&#xff01;全新 DALL-E 3 必须掌握的 20 种公式使用方法上周末&#xff0c;DALL-E 3 正式加入 ChatGpt&…

playwright vscode 插件源码解析

Playwright vscode插件主要功能 Playwright是微软开发的一款主要用于UI自动化测试的工具&#xff0c;在vscode中上安装playwright vscode插件&#xff0c;可以运行&#xff0c;录制UI自动化测试。 playwright vscode插件主要包括两块功能&#xff0c;功能一是在Test Explorer中…

excel字符串列的文本分列合并

excel表有两列&#xff0c;第一列是“姓名”&#xff0c;第二列是“诊断”&#xff0c;有高血压、糖尿病等。我想出一个统计表&#xff0c;将每个人的诊断分为1-N列&#xff0c;比如张三&#xff0c;第一诊断高血压&#xff0c;第二诊断糖尿病&#xff0c;分列显示。我们可以用…

七人团购新体验:解锁数字时代购物新篇章

在数字化浪潮的推动下&#xff0c;购物体验正迈向新的里程碑。其中&#xff0c;七人团购模式以其独特的魅力和创新性&#xff0c;为消费者带来了前所未有的实惠与便利。现在&#xff0c;让我们一同探索这一新兴购物模式的运作机制与潜在价值&#xff0c;特别是针对一款标价599元…

Excel如果将一个表格拆分为多个表格,文末另赠彩蛋!

前期分享如何用数据透视表将一个表格拆分成多个工作薄Excel一个表格拆分多个表格&#xff0c;你学会了吗&#xff1f; 今天刘小生分享另外一种&#xff0c;如果拆分成多个工作表格文件&#xff01; 如何将一个表格根据部门进行拆分成多个表格&#xff0c;再点对点发送给各部门…

MacBook Pro 忘记root用户密码,重置密码步骤

一、以普通用户名登录系统&#xff0c;并打开terminal终端&#xff0c; 输入&#xff1a;sudo bash sudo bash Password:*****&#xff08;输入当前用户的密码&#xff09; 成功后进入bash-3.2#的命令模式 二、在bash-3.2#命令模式下 输入&#xff1a;sudo passwd root sud…

代码随想录算法训练营第三十三天|452. 用最少数量的箭引爆气球、 435. 无重叠区间、 763.划分字母区间

452. 用最少数量的箭引爆气球 题目链接&#xff1a;452. 用最少数量的箭引爆气球 文档讲解&#xff1a;代码随想录 状态&#xff1a;没想出来 思路&#xff1a;对气球终点位置排序&#xff0c;从第一个气球终点位置射出箭&#xff0c;看这支箭可以尽可能穿过几个气球&#xff0…

一文搞懂Linux多线程【上】

目录 &#x1f6a9;引言 &#x1f6a9;再次理解页表 &#x1f6a9;初识线程 &#x1f6a9;线程和pthread库 &#x1f680;线程创建 &#x1f6a9;线程的资源共享问题 &#x1f6a9;线程的优缺点 &#x1f6a9;引言 今天&#xff0c;我们开始学习Linux中的线程部分。Lin…

中国车牌检测数据集VOC+YOLO格式2001张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2001 标注数量(xml文件个数)&#xff1a;2001 标注数量(txt文件个数)&#xff1a;2001 标注…

了解SD-WAN与传统WAN的区别

近年来&#xff0c;许多企业选择了SD-WAN作为他们的网络解决方案。云基础架构的SD-WAN不仅具备成本效益&#xff0c;而且提供更安全、更可靠的WAN连接&#xff0c;有助于实现持续盈利。客户能够更好地控制他们的网络&#xff0c;个性化定制且无需额外成本。 那么&#xff0c;为…

Golang逃逸分析

在Go语言中&#xff0c;逃逸分析(Escape Analysis)是一种编译器优化技术&#xff0c;用于确定变量是应该分配在堆上还是在栈上。这对程序的性能有显著的影响&#xff0c;因为栈上资源的分配速度和释放速度要比堆上快得多&#xff0c;同时堆上的内存管理也更加简单。 基本概念 …

快速鲁棒的 ICP (Fast and Robust Iterative Closest Point)

迭代最近点&#xff08;Iterative Closet Point&#xff0c;ICP&#xff09;算法及其变体是两个点集之间刚性配准的基本技术&#xff0c;在机器人技术和三维重建等领域有着广泛的应用。ICP的主要缺点是&#xff1a;收敛速度慢&#xff0c;以及对异常值、缺失数据和部分重叠的敏…

应用监控eBPF 版调研

参考&#xff1a; https://www.toutiao.com/article/7327353509735596559/?appnews_articletamp1717488680&use_new_style1&req_id20240604161119838096AAE4AD4F44788E&group_id7327353509735596559&wxshare_count1&tt_fromweixin&utm_sourceweixin&…

CentOS7.6安装RabbitMQ

前言&#xff1a;因为RabbitMQ是ERlang语言编写所以要先安装ERlang再安装RabbitMQ 安装ERlang 借鉴前辈原文地址&#xff1a;https://www.cnblogs.com/fengyumeng/p/11133924.html 第一步&#xff1a;安装依赖 yum -y install gcc glibc-devel make ncurses-devel open…

PDF秒变翻页式电子画册

​在当今数字化时代&#xff0c;将PDF文档转换成翻页式电子画册是一种提升作品展示效果和传播效率的有效方式。以下是将PDF秒变翻页式电子画册的攻略&#xff0c;帮助您轻松掌握数字创作技巧。 首先&#xff0c;选择一个合适的制作工具是关键。目前市场上有多种在线平台和软件可…

HTML5休闲小游戏《猫猫超市》源码,引流、刷广告利器

HTML5休闲小游戏《猫猫超市》源码&#xff0c;直接把源码上传到服务器就能使用了&#xff01; 下载链接&#xff1a;https://www.huzhan.com/code/goods467910.html

k8s如何使用 HPA 实现自动扩展

使用Horizontal Pod Autoscaler (HPA) 实验目标&#xff1a; 学习如何使用 HPA 实现自动扩展。 实验步骤&#xff1a; 创建一个 Deployment&#xff0c;并设置 CPU 或内存的资源请求。创建一个 HPA&#xff0c;设置扩展策略。生成负载&#xff0c;观察 HPA 如何自动扩展 Pod…