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滚动视图复用
发现问题 在游戏开发中有一个常见的需求,就是需要在屏幕显示多个(多达上百)显示item,然后用户用手指滚动视图可以选择需要查看的item。 现在的情况是在100个data的时候,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.总结 承接上文:详解Spring AOP(一&…...
sql-analysis
文章目录 痛点: 1、无法提前发现慢sql,可能恶化为慢sql的语句 2、线上出现慢sql后,无法快速止损 后果:一般是以响应时间来发现慢sql,这时候已经对业务产生了一定影响,这时候就要改代码重新发布上线或者改数…...
后台管理台字典localStorage缓存删除
localStorage里存放了如以下dictItems_开头的字典数据,localStorage缓存是没有过期时间的,需要手动删除。同时localStorage里还存有其他不需要删除的数据。 这里的方案是遍历localStorage,利用正则和所有key进行匹配,匹配到dict…...
计算机毕业设计PySpark+Hadoop招聘推荐系统 招聘大数据 招聘数据分析 招聘可视化 大数据毕业设计 大数据毕设
1. 管理端: 带有职位的增删改查功能,评论功能是针对新闻模块的,类似新闻大数据的实现 2. 网站端: python / java 协同过滤推荐算法 / 下载职位数据表收费1元每条 / 账户充值 / 短信验证码修改密码 / 身份证识别 / 多条件搜索 3.…...
.Net预定义的泛型委托
我们每次要使用一个委托前,都需要自定义这个委托类型,声明其参数和返回值,然后才能实例化委托类型的对象、最后调用委托对象。 为了简化这个过程,.Net预定义了Func<T>委托、Action<T>委托类型和Predicate<T>&a…...
Unity的Excel转表工具
该Excel工具主要由Python语言完成,版本为3.x 主要功能: 1.转换后的数据存储结构为二进制。 2.excel文件可以选择多种数据类型:int、float、string、一维(int、float、string)、二维int、Map(int/int、in…...
静态随机存储器(SRAM):高速缓存的奥秘
目录 基本的静态存储单元阵列 基本的SRAM逻辑结构 1. 概述 2. SRAM阵列 3. 行选择器(Row Decoder) 4. 列选择器(Column Decoder) 5. 读写电路 6. 控制电路 7. 工作过程 SRAM的读/写时序 SRAM 结构概述 读操作时序 读…...
Linux CentOS 7 服务器集群硬件常用查看命令
(一)查看内核: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(二)查看系统:cat /etc/redhat-releas…...
《Windows API每日一练》5.4 键盘消息和字符集
本节我们将通过实例来说明不同国家的语言、字符集和字体之间的差异,以及Windows系统是如何处理的。 本节必须掌握的知识点: 第31练:显示键盘消息 非英语键盘问题 字符集和字体 第32练:显示默认字体信息 第33练:创建逻…...
【uniapp】uniapp开发微信小程序入门教程
HBuilderx中uniapp开发微信小程序入门教程 一、 环境搭建 1. HBuilderx下载安装 HBuilderx下载安装地址 2. 微信开发者工具下载安装 微信开发者工地址具下载安装 二、创建uniapp项目 选择:文件>新建>项目>uni-app 输入项目名称>选择默认模板>…...
Python爬虫项目集:豆瓣电影排行榜top250
关于整理日常练习的一些爬虫小练习,可用作学习使用。 爬取项目以学习为主,尽可能使用更多的模块进行练习,而不是最优解。 爬虫概要 示例python 库爬取模块request解析模块BeautifulSoup存储类型list(方便存入数据库)…...
34-Openwrt uhttpd与rpcd
uhttpd作为一个简单的web服务器,其代码量并不多,而且组织结构比较清楚。和其它网络服务器差不多,其main函数进行一些初始化(首先parse config-file,然后parse argv),然后进入一个循环࿰…...
uni app 树状结构数据展示
树状数据展示,可以点击item 将点击数据给父组件 ,满足自己需求。不喜勿喷,很简单可以根据自己需求改哈,不要问,点赞收藏就好。其实可以和上一篇文章uni app 自定义 带popup弹窗的input组件-CSDN博客结合使用ÿ…...
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表示用户名 后是机器的名字 ~表示文件夹,刚上来是默认的用户目录 ls -a 可以显示出隐藏的文件 蓝色的表示文件夹 白色的是文件 ll -a 查看详细信息 total表示所占磁盘总大小 一般以KB为单位 d开头表示文件夹 -代表文件 后面得三组rwx分别对应管理员用户-组…...
竞赛选题 python+opencv+深度学习实现二维码识别
0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 pythonopencv深度学习实现二维码识别 🥇学长这里给一个题目综合评分(每项满分5分) 难度系数:3分工作量:3分创新点:3分 该项目较为新颖&…...
Java读取指定 JAR 包路径中的 git.properties 文件
Java读取指定 JAR 包路径中的 git.properties 文件 在上述代码中,首先打开 JAR 文件,获取 git.properties 文件的 JarEntry 对象,如果存在该条目,就获取其输入流进行后续的读取和处理。具体的读取和处理逻辑需要根据您的实际需求在…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
以光量子为例,详解量子获取方式
光量子技术获取量子比特可在室温下进行。该方式有望通过与名为硅光子学(silicon photonics)的光波导(optical waveguide)芯片制造技术和光纤等光通信技术相结合来实现量子计算机。量子力学中,光既是波又是粒子。光子本…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...
Unity UGUI Button事件流程
场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...
【SpringBoot自动化部署】
SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...
MySQL的pymysql操作
本章是MySQL的最后一章,MySQL到此完结,下一站Hadoop!!! 这章很简单,完整代码在最后,详细讲解之前python课程里面也有,感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...
