当前位置: 首页 > news >正文

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;// 道具idpublic 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 showpublic enum ItemLayoutType{// 最后一位表示滚动方向Vertical = 1,                   // 0001Horizontal = 2,                 // 0010VerticalThenHorizontal = 4,     // 0100HorizontalThenVertical = 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个临界indexprotected int[] criticalItemIndex = new int[4];Rect refRect;// resource managementSimpleObjPool<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;// statusprivate 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; // 0011InternalUpdateData();}else{if(willUpdateData == 0 && gameObject.active){StartCoroutine(DelayUpdateData());}willUpdateData |= 3;}}public void UpdateDataIncrementally(bool immediately = true){if (!initialized){InitScrollView();}if (immediately){willUpdateData |= 1; // 0001InternalUpdateData();}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){// verticalfloat value = 1 - (-r.yMax / (content.sizeDelta.y - refRect.height));//value = Mathf.Clamp01(value);SetNormalizedPosition(value, 1);}else{// horizontalfloat 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){// 重置所有rectitemWithRect.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){// 重置所有rectif(!keepOldItems){managedItems[i].rectDirty = true;}if(i == newDataCount - 1){managedItems[i].rectDirty = true;}}// 超出部分 清理回收itemif (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){// 重置所有rectmanagedItems[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的 rectviewRect.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之前的最近的已更新的rectint 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;}//选择Itempublic 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的positionint 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 responseprivate 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;// 是否启用时间文本UIprivate bool _isIcon;// 是否启用Iconprivate 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);elsetxtTime.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脚本的显示对象,如下图所示。

最后

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

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

相关文章:

Unity的ScrollView滚动视图复用

发现问题 在游戏开发中有一个常见的需求&#xff0c;就是需要在屏幕显示多个&#xff08;多达上百&#xff09;显示item&#xff0c;然后用户用手指滚动视图可以选择需要查看的item。 现在的情况是在100个data的时候&#xff0c;Unity引擎是直接创建出对应的100个显示item。 …...

详解Spring AOP(二)

目录 1.切点表达式 1.1execution表达式 1.2 annotation 1.2.1自定义注解MyAspect 1.2.3添加自定义注解 2.Sping AOP原理 2.1代理模式 2.1.1静态代理 2.1.2动态代理 2.1.3JDK动态代理 2.1.4CGLIB动态代理 3.总结 承接上文&#xff1a;详解Spring AOP&#xff08;一&…...

sql-analysis

文章目录 痛点&#xff1a; 1、无法提前发现慢sql&#xff0c;可能恶化为慢sql的语句 2、线上出现慢sql后&#xff0c;无法快速止损 后果&#xff1a;一般是以响应时间来发现慢sql&#xff0c;这时候已经对业务产生了一定影响&#xff0c;这时候就要改代码重新发布上线或者改数…...

后台管理台字典localStorage缓存删除

localStorage里存放了如以下dictItems_开头的字典数据&#xff0c;localStorage缓存是没有过期时间的&#xff0c;需要手动删除。同时localStorage里还存有其他不需要删除的数据。 这里的方案是遍历localStorage&#xff0c;利用正则和所有key进行匹配&#xff0c;匹配到dict…...

计算机毕业设计PySpark+Hadoop招聘推荐系统 招聘大数据 招聘数据分析 招聘可视化 大数据毕业设计 大数据毕设

1. 管理端&#xff1a; 带有职位的增删改查功能&#xff0c;评论功能是针对新闻模块的&#xff0c;类似新闻大数据的实现 2. 网站端&#xff1a; python / java 协同过滤推荐算法 / 下载职位数据表收费1元每条 / 账户充值 / 短信验证码修改密码 / 身份证识别 / 多条件搜索 3.…...

.Net预定义的泛型委托

我们每次要使用一个委托前&#xff0c;都需要自定义这个委托类型&#xff0c;声明其参数和返回值&#xff0c;然后才能实例化委托类型的对象、最后调用委托对象。 为了简化这个过程&#xff0c;.Net预定义了Func<T>委托、Action<T>委托类型和Predicate<T>&a…...

Unity的Excel转表工具

该Excel工具主要由Python语言完成&#xff0c;版本为3.x 主要功能&#xff1a; 1.转换后的数据存储结构为二进制。 2.excel文件可以选择多种数据类型&#xff1a;int、float、string、一维&#xff08;int、float、string&#xff09;、二维int、Map&#xff08;int/int、in…...

静态随机存储器(SRAM):高速缓存的奥秘

目录 基本的静态存储单元阵列 基本的SRAM逻辑结构 1. 概述 2. SRAM阵列 3. 行选择器&#xff08;Row Decoder&#xff09; 4. 列选择器&#xff08;Column Decoder&#xff09; 5. 读写电路 6. 控制电路 7. 工作过程 SRAM的读/写时序 SRAM 结构概述 读操作时序 读…...

Linux CentOS 7 服务器集群硬件常用查看命令

&#xff08;一&#xff09;查看内核&#xff1a;uname -a [rootcdh1 ~]# uname -a Linux cdh1.macro.com 3.10.0-1062.el7.x86_64 #1 SMP Wed Aug 7 18:08:02 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux&#xff08;二&#xff09;查看系统&#xff1a;cat /etc/redhat-releas…...

《Windows API每日一练》5.4 键盘消息和字符集

本节我们将通过实例来说明不同国家的语言、字符集和字体之间的差异&#xff0c;以及Windows系统是如何处理的。 本节必须掌握的知识点&#xff1a; 第31练&#xff1a;显示键盘消息 非英语键盘问题 字符集和字体 第32练&#xff1a;显示默认字体信息 第33练&#xff1a;创建逻…...

【uniapp】uniapp开发微信小程序入门教程

HBuilderx中uniapp开发微信小程序入门教程 一、 环境搭建 1. HBuilderx下载安装 HBuilderx下载安装地址 2. 微信开发者工具下载安装 微信开发者工地址具下载安装 二、创建uniapp项目 选择&#xff1a;文件>新建>项目>uni-app 输入项目名称>选择默认模板>…...

Python爬虫项目集:豆瓣电影排行榜top250

关于整理日常练习的一些爬虫小练习&#xff0c;可用作学习使用。 爬取项目以学习为主&#xff0c;尽可能使用更多的模块进行练习&#xff0c;而不是最优解。 爬虫概要 示例python 库爬取模块request解析模块BeautifulSoup存储类型list&#xff08;方便存入数据库&#xff09…...

34-Openwrt uhttpd与rpcd

uhttpd作为一个简单的web服务器&#xff0c;其代码量并不多&#xff0c;而且组织结构比较清楚。和其它网络服务器差不多&#xff0c;其main函数进行一些初始化&#xff08;首先parse config-file&#xff0c;然后parse argv&#xff09;&#xff0c;然后进入一个循环&#xff0…...

uni app 树状结构数据展示

树状数据展示&#xff0c;可以点击item 将点击数据给父组件 &#xff0c;满足自己需求。不喜勿喷&#xff0c;很简单可以根据自己需求改哈&#xff0c;不要问&#xff0c;点赞收藏就好。其实可以和上一篇文章uni app 自定义 带popup弹窗的input组件-CSDN博客结合使用&#xff…...

KVM在线yum源部署-centos 7

一、虚拟化简介 虚拟化就是操作系统里嵌套操作系统,一台服务器买回来,可能只是用作一个http服务,资源不能充分利用,而虚拟化的诞生有效解决了这个问题,以硬件资源上使用虚拟化,实现单硬件多系统,充分挖掘硬件性能,节能增效。同时通过多年的改进发展,虚拟化进化成云服务…...

TSF的服务发现与Consul有何区别?

TSF(腾讯服务框架)和Consul都是用于服务发现的工具,但它们在设计理念、功能特性、集成方式等方面存在一些区别。 ### 设计理念和目标 **Consul** 是一个开源的工具,用于服务发现、配置和分段。它提供了一种简单的方式来注册和发现服务,以及健康检查和键值存储功能。Consul…...

kotlin集合框架

1、集合框架的接口类型对比 2、不可变和可变List fun main() {// 不可变List - 不能删除或添加元素val intList: List<Int> listOf(1,2,3)intList.forEach{println(it) // 1 2 3}println("")// 可变List - 可以删除或添加元素val mutableList mutableListO…...

服务器(Linux系统的使用)——自学习梳理

root表示用户名 后是机器的名字 ~表示文件夹&#xff0c;刚上来是默认的用户目录 ls -a 可以显示出隐藏的文件 蓝色的表示文件夹 白色的是文件 ll -a 查看详细信息 total表示所占磁盘总大小 一般以KB为单位 d开头表示文件夹 -代表文件 后面得三组rwx分别对应管理员用户-组…...

竞赛选题 python+opencv+深度学习实现二维码识别

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; pythonopencv深度学习实现二维码识别 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;3分 该项目较为新颖&…...

Java读取指定 JAR 包路径中的 git.properties 文件

Java读取指定 JAR 包路径中的 git.properties 文件 在上述代码中&#xff0c;首先打开 JAR 文件&#xff0c;获取 git.properties 文件的 JarEntry 对象&#xff0c;如果存在该条目&#xff0c;就获取其输入流进行后续的读取和处理。具体的读取和处理逻辑需要根据您的实际需求在…...

逻辑回归(Logistic Regression)及其在机器学习中的应用

&#x1f680;时空传送门 &#x1f50d;逻辑回归原理&#x1f4d5;Sigmoid函数&#x1f388;逻辑回归模型 &#x1f4d5;损失函数与优化&#x1f388;损失函数&#x1f680;优化算法 &#x1f50d;逻辑回归的应用场景&#x1f340;使用逻辑回归预测客户流失使用scikit-learn库实…...

【计算机视觉】人脸算法之图像处理基础知识【七】

直方图均衡化 直方图均衡化是一种常用的图像处理技术&#xff0c;用于改善图像的对比度&#xff0c;特别是在图像的细节被埋没在暗部或亮部区域时。通过重新分配图像的像素强度值&#xff0c;使得图像的整体对比度增强&#xff0c;从而让更多的细节变得可见。 import cv2 imp…...

家政预约小程序14权限配置

目录 1 创建用户2 创建角色3 启用登录4 实现退出总结 我们现在小程序端的功能基本开发好了&#xff0c;小程序开发好之后需要给运营人员提供管理后台&#xff0c;要分配账号、配置权限&#xff0c;我们本篇就介绍一下权限如何分配。 1 创建用户 在微搭中&#xff0c;用户分为内…...

解决 vue 项目一直出现 sockjs-node/info?t=问题

其实如果是在开发环境&#xff0c;应该是开发的时候网络环境变更导致&#xff0c;比如你切换无线网络&#xff0c;导致开发服务器的IP地址换了&#xff0c;这样开发服务器会不知道如何确定访问源。开发环境中关闭npm dev server&#xff0c;然后重新npm run serve重新构建服务环…...

麒麟信安系统关闭core文件操作

在使用麒麟信安系统时&#xff0c;如果应用程序运行过程中崩溃了&#xff0c;此时并不会导致内核崩溃&#xff0c;只会在tmp目录下产生崩溃数据&#xff0c;如下图 不过tmp目录下的分区容量有限&#xff0c;当崩溃的应用core文件过大时将会占用tmp空间&#xff0c;导致tmpfs分区…...

微信小程序轮播图

效果图 详情可见 微信小程序 参照&#xff1a;swiper | uni-app官网 代码&#xff1a; <!--轮播图-- > <swiper interval"2000" autoplay"true" circular"true" style"height: 300px;"><swiper-item style&qu…...

redisson WRONGPASS invalid username-password pair or user is disable

1、技术架构&#xff1a;若依微服务框架 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2021.1</version></dependency> <dependency><…...

QT拖放事件之一:初识拖放4大事件处理函数

0、拖放 两个动作,合在一起称之为拖放事件; 拖:就是拖着走; 放:就是拖着走,然后松开鼠标了,释放了,这就是放; 注意:放:拖着的东西要放在什么地方??? 假如,我将一个记事本拖着跑,然后放到一个Widget窗口上,那么为了使得Widget能感知相应的事件(拖着进入事件…...

使用Python进行数据可视化:从基础到高级

使用Python进行数据可视化:从基础到高级 数据可视化是数据分析过程中不可或缺的一部分,通过图形化的方式展示数据,可以更直观地发现数据中的趋势和模式。Python凭借其丰富的库和强大的功能,成为数据可视化的首选编程语言。本文将介绍数据可视化的基础概念、常用的Python库…...

【十二】图解 Spring 核心数据结构:BeanDefinition

图解 Spring 核心数据结构&#xff1a;BeanDefinition 简介 使用spring框架的技术人员都知道spring两个大核心技术IOC和AOP&#xff0c;随着投入更多的时间去学习spring生态&#xff0c;越发觉得spring的发展不可思议&#xff0c;一直都是引领着Java EE的技术变革&#xff0c;这…...