Unity的ScrollView滚动视图复用

news2024/11/16 17:52:38

发现问题

在游戏开发中有一个常见的需求,就是需要在屏幕显示多个(多达上百)显示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脚本的显示对象,如下图所示。

最后

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

0801功率放大问题

3个学时讲一个电路&#xff08;两个共集共集并联&#xff09;&#xff0c;4个问题&#xff0c;发展线索 丙类放大电路用在高频通讯行业&#xff0c;低频功放是甲类&#xff0c;乙类&#xff0c;甲乙类 PT三极管的损耗 Pv电源提供的功率 现代模电通常使用方法b 只有交流…

聊聊 golang 中 channel

1、引言 Do not communicate by sharing memory; instead, share memory by communicating Golang 的并发哲学是“不要通过共享内存进行通信&#xff0c;而要通过通信来共享内存”&#xff0c;提倡通过 channel 进行 goroutine 之间的数据传递和同步&#xff0c;而不是通过共享…

YashanDB为新质生产力赋能 灌注合肥区域转型源动力

当前&#xff0c;数据要素已成为我国数字经济的“核心引擎”与“关键生产要素”&#xff0c;为全面激发数据要素的价值&#xff0c;各地区正积极探索数据要素交易平台的可行模式&#xff0c;加快在数据要素领域的布局。近日&#xff0c;深圳计算科学研究院崖山数据库系列产品受…

JDBC从入门到精通-笔记(一):JDBC基本概念与开发基础

视频资源&#xff1a;JDBC从入门到精通视频教程-JDBC实战精讲_哔哩哔哩_bilibili JDBC定义与本质 概念 什么是JDBC&#xff1a;Java DataBase Connectivity JDBC本质&#xff1a;SUN公司制定的一套接口&#xff08;interface&#xff09;&#xff0c;java.sql.*。 面向接口调…

【progressBar-js】优雅的 前端进度条 构建!

progressBar-js JS 前端进度条小工具 您可以通过此工具来构建一个有效的工具条&#xff0c;接下来就是一个示例&#xff01; 使用示例 引入 progressBar-js 库 直接在这里将 css 和 js 文件引入进来就算是成功导入了哦&#xff01;&#xff01;&#xff01; <link href&…

SVN学习(001 svn安装)

尚硅谷SVN高级教程(svn操作详解) 总时长 4:53:00 共72P 此文章包含第1p-第p19的内容 介绍 为什么使用版本控制工具 版本控制工具的功能 版本控制简介 客户端服务器结构 c/s结构 服务端的结构&#xff1a; 服务程序 、版本库(存放我们上传的文件) 客户端的三个基本操作&#…

高考填报志愿,选专业和选学校,哪个优先?

一、 专业优先&#xff0c;还是学校优先&#xff1f; 专业和学校都非常重要&#xff0c;好的学校可以给你提供较高的学习平台&#xff0c;好的专业能够给将来的职业生涯提供便利。高考报考&#xff0c;每一个学校的每一个专业的分数都会不同&#xff0c;热门的专业分数较高&am…

Swift 周报 第五十三期

文章目录 前言新闻和社区苹果公司取得基于波束组合的信道状态信息&#xff08;CSI&#xff09;反馈专利&#xff0c;为 5G 网络中的信道状态信息&#xff08;CSI&#xff09;报告提供新方案关于在欧盟分发 App 的最新信息公司快评&#xff5c;新广告引发不满&#xff0c;苹果也…

大模型揭秘:AI与CatGPT在实体识别中的创新应用

摘要 尽管大规模语言模型 (LLM) 在各种 NLP 任务上已经取得了 SOTA 性能&#xff0c;但它在 NER 上的性能仍然明显低于监督基线。这是由于 NER 和 LLMs 这两个任务之间的差距&#xff1a;前者本质上是序列标记任务&#xff0c;而后者是文本生成模型。在本文中&#xff0c;我们…

被年轻人买爆的转运能量石,戴一天竟等于拍千次胸片?

离谱的事年年有&#xff0c;这几年可以说非常多&#xff01;‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍ 尤其是这届年轻人&#xff0c;不知道什么时候开始&#xff0c;越来越迷信了&#xff01; 比如去年很…

基于在校学习平台MOOC的选课推荐系统

基于在校学习平台MOOC的选课推荐系统 1、效果 在线demo&#xff0c;点我查看 2、功能 根据学生于在校学习平台MOOC学习期间的选课记录等相关特征来对学生进行课程推荐。 采用数据挖掘技术&#xff0c;包括BPR、FM、CF&#xff0c;神经网络推荐&#xff0c;用户协同过滤推荐…

2024 CISCN 华东北分区赛-Ahisec

Ahisec战队 WEB python-1 break 源码如下&#xff1a; # -*- coding: UTF-8 -*-from flask import Flask, request,render_template,render_template_stringapp Flask(__name__)def blacklist(name):blacklists ["print","cat","flag",&q…

【嵌入式Linux】<总览> 多进程(更新中)

文章目录 前言 一、进程的概念与结构 1. 相关概念 2. 内核区中的进程结构 3. 进程的状态 4. 获取进程ID函数 二、进程创建 1. fork和vfork函数 2. 额外注意点 3. 构建进程链 4.构建进程扇 三、进程终止 1. C程序的启动过程 2. 进程终止方式 四、特殊的进程 1. 僵…

AppInventor2添加超过10个屏幕会怎样?

之前发过一篇AppInventor2官方翻译文档&#xff0c;建议一个项目不要超过10个屏幕&#xff0c;详见&#xff1a; App Inventor 2 构建多屏幕App的最佳实践 App Inventor 可以轻松地向应用程序添加更多屏幕&#xff0c;但最好也不要添加太多屏幕&#xff0c;因为多个屏幕的应用…

U盘数据恢复全攻略:从原理到实践

一、引言&#xff1a;为何U盘数据恢复至关重要 在信息化时代&#xff0c;U盘作为便携存储设备&#xff0c;广泛应用于各个领域。然而&#xff0c;U盘数据的丢失往往给个人和企业带来极大的困扰。数据丢失的原因多种多样&#xff0c;可能是误删除、格式化、文件系统损坏&#x…

探索约束LLM输出JSON的应用

0、 引言 JSON&#xff08;JavaScript Object Notation&#xff09;因其简洁、易读和易于解析的特性&#xff0c;已成为全球使用最广泛的数据交换格式之一。它能够满足各种数据交换需求&#xff0c;特别是在构建人工智能驱动的应用程序时&#xff0c;工程师们经常需要将大型语…

Jenkins教程-8-上下游关联自动化测试任务构建

上一小节小节我们学习了一下Jenkins自动化测试任务发送测试结果邮件的方法&#xff0c;本小节我们讲解一下Jenkins上下游关联自动化测试任务的构建。 下面我们以一个真实的自动化测试场景来讲解Jenkins如何管理上下游关联任务的触发和构建&#xff0c;比如我们有两个jenkin任务…

基础入门篇 | YOLOv10 项目【训练】【验证】【推理】最简单教程 | YOLOv10必看 | 最新更新,直接打印 FPS,mAP50,75,95

文章目录 训练 --train.py推理 --detect.py验证 --val.py不训练,只查看模型结构/参数量/计算量 --test.pyYOLOv10 是基于 YOLOv8 项目的改进版本,目前已经被 YOLOv8 项目合并,所以两个算法使用方法完全一致~ 今天我给大家展示一种非常方便的使用过程,包含【训练】【验证】…

情绪管理篇:让七情自然流露,不过分压抑也不掺杂极端的想法即可来去自如

情绪管理篇&#xff1a; 人有七情&#xff0c;本属常理&#xff0c;该哭的时候哭、该笑的时候笑、该怒的时候怒、该忧的时候忧 学习圣贤之学&#xff0c;并非让我们像木头人一样&#xff0c;枯木死灰&#xff0c;而要让自己不要被七情所缠缚、被七情所乱心&#xff0c;我们的喜…

QT拖放事件之三:自定义拖放操作-利用QDrag来拖动完成数据的传输

1、运行效果 1)Qt::MoveAction 2)Qt::CopyAction 2、源码 #include "Widget.h" #include "ui_Widget.h" #include "common.h"