制作一个简易的UGUI无限滑动框(Unity)
市面上有很多这种无限拖拽的插件 但是功能细化的太严重了 改的话有些耗时 如果没有太多严苛的需求没必要改工程量比较大的插件 完全可以自己写一个
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ItemRect { public Rect mRect; //格子的索引 因为是用来关联位置的 所以不存在删除或修改操作 public readonly int itemIndex; //如果这个位置上存在显示的UI那么将格子的isUse属性设置为true public bool isUse = false; public ItemRect(float x,float y,float width,float height,int itemIndex) { mRect = new Rect(x,y,width,height); this.itemIndex = itemIndex; } } public interface IItemExecute { //刷新数据 void Execute_FinshData(); //绑定UI void BindUgui(); } //抽象任何地方出每一个滑动单元的公共方法 public class ItemPropoty : MonoBehaviour, IItemExecute { //储存的数据 public object Data; public object DataKey; public ItemRect mDRect; public delegate void FreshFunc(int index); //在初始化的时候给到刷新数据的方法 public FreshFunc onUpdata; public virtual void Execute_FinshData() { BindUgui(); if (onUpdata != null) onUpdata(mDRect.itemIndex); } public void Execute_PlacePos() { (transform as RectTransform).sizeDelta = mDRect.mRect.size; (transform as RectTransform).localPosition = mDRect.mRect.position; BindUgui(); } /// <summary> /// 绑定UGUI 用名称查询的方式绑定UI类索引 /// </summary> public virtual void BindUgui() { } } /// <summary> /// 定制滑动单元 /// </summary>
这个用来给定格子位置 在初始化的时候就确认所有格子的位置 这样不用动态的计算
这里我把那个UI的绑定设计为 尽量使用名字去查询UI 在bindUgui中去查询和添加按钮还有一些其他的回调
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class ItemUnit_1 : ItemPropoty { Button btn; Text txt; public override void BindUgui() { btn = transform.GetComponentInChildren<Button>(); txt = transform.GetComponentInChildren<Text>(); txt.text = mDRect.itemIndex.ToString(); btn.onClick.AddListener(() => { txt.text = mDRect.itemIndex + " : " + mDRect.itemIndex; }); } public void AddImage() { } public void ChangeImage() { } }
把ItemPropoty放在拖拽的单元上面
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using System.Linq; public class CustomScroll : MonoBehaviour { //用于显示的单元 public GameObject itemUnit; private RectTransform item; //这个用来存储UI显示的实例 private List<Transform> itemList; //记录所有ui应该显示的位置 private Dictionary<int, ItemRect> itemRectDic; //显示的画布 public RectTransform mDisplayRect; //上拉是否还有显示单位 private bool? isHavePrevious = null; public bool? IsHavePrevious { get => isHavePrevious; } //下拉是否还有显示单位 private bool? isHaveSubsequent = null; public bool? IsHaveSubsequent { get => isHaveSubsequent; } private ScrollRect scrollRect; //显示的行数 需要计算 private int scrollRow; public int ScrollRow { get => scrollRow; } [Header("行间隙")] public float interval_Veritical; public float Interval_Veritical { get => interval_Veritical + item.rect.height; } //显示的列数 private int scrollColumn; public int ScrollColumn { get => scrollColumn; } [Header("列间隙")] public float interval_Horizontal; public float Interval_Horizontal { get => interval_Horizontal + item.rect.width;} void Start() { IntializeScroll(); IntializeDisplayCount(2, 30); //TODO FIX 测试用 isLoad = true; } //做一些初始化的任务 private void IntializeScroll() { itemList = new List<Transform>(); waitChangeList = new List<ItemPropoty>(); //GetComponentInParent<ScrollRect>().movementType = ScrollRect.MovementType.Clamped; scrollRect = GetComponentInParent<ScrollRect>(); itemRectDic = new Dictionary<int, ItemRect>(); item = itemUnit.transform as RectTransform; //初始化位置记录 previousPagePos = transform.position.y; } /// <summary> /// 先计算出显示的行列 /// </summary> public void IntializeDisplayCount(int columnCount, int itemCount) { SetContentSizeDelta(columnCount, itemCount); IntializeDisplayCount(scrollRow, scrollColumn, itemCount); } private void SetContentSizeDelta(int columnCount, int itemCount) { //若乱设置0或复负数直接按1处理 scrollColumn = Mathf.Max(1, columnCount); //计算出显示的最大行数 item的长度 scrollRow = itemCount / scrollColumn + 1; //设置显示画布的大小 (transform as RectTransform).sizeDelta = new Vector2( scrollColumn * Interval_Horizontal, (scrollRow - 1) * Interval_Veritical); } // public void IntializeDisplayCount(int rowCount,int columnCount, int itemCount) { //先初始化好格子的位置 在初始化位置的同时设置好显示内容的尺寸 AddItemPosition(itemCount); //计算当前显示的数量 存在一个占半格子的问题 暂时未处理 可以使用rect.Overlaps 查看是否相交 根据相交的位置判断加的排数 var pageSize = (transform.parent.parent as RectTransform).sizeDelta; //因为间距的问题没处理所以临时加2 var pageArea = (int)(pageSize.x / Interval_Horizontal)* (int)(pageSize.y / Interval_Veritical); //TODO FIX int maxDsiplayNum = (int)pageArea + scrollColumn+2; //Debug.Log("当前最大的显示数量 : "+maxDsiplayNum); for (int i = 0; i < scrollRow && i< maxDsiplayNum; i++) { Transform tmpItem = GameObject.Instantiate(itemUnit).transform; itemList.Add(tmpItem); tmpItem.localScale = Vector3.one; tmpItem.SetParent(transform, false); //tmpItem.gameObject.AddComponent<ItemUnit_1>(); //tmpItem.gameObject.layer = mDisplayRect.gameObject.layer; tmpItem.gameObject.SetActive(false); tmpItem.name = i.ToString(); } BenginDisplay(); } /// <summary> /// 开始显示UI单元 /// </summary> public void BenginDisplay() { //标记显示头 for (int i = 0; i < itemRectDic.Count; i++) { if (i == itemList.Count) break; //得到物体身上的属性 var tmp = itemList[i].GetComponent<ItemPropoty>(); //拿到对应的值 itemRectDic.TryValueGet(i,out tmp.mDRect); //给定位置 tmp.Execute_PlacePos(); //将他设置为可见 itemList[i].gameObject.SetActive(true); tmp.mDRect.isUse = true; } } private int allItemCount = 0; public int AllItemCount { get => allItemCount; } [Header("x,y轴的初始边距")] public int distance_x = 100; public int distance_y = -50; /// <summary> /// 判断元数是否为追加 /// </summary> /// <param name="isSuperaddition"></param> public void AddItemPosition(int nxtCount) { int curRow,curColumn; int tmp = itemRectDic.Count; allItemCount += nxtCount; SetContentSizeDelta(scrollColumn,allItemCount); for (int i = tmp; i < tmp + nxtCount; i++) { curRow = i / scrollColumn; curColumn = i % scrollColumn; var itemPos = new ItemRect(curColumn*Interval_Horizontal + distance_x, -curRow*Interval_Veritical + distance_y, item.rect.width, item.rect.height, i); itemRectDic.Add(i,itemPos); } } /// <summary> /// 查询所有排位置的最值元素 /// 做个优化 查询每一排的第一个元素就行了 这样换排的时候 如果是上一排就用index-1 下一排就是 index+scrollColumn(列数) /// </summary> /// <param name="isMax"></param> /// <returns></returns> public Transform GetBestValueItem(bool isMax) { if (itemList == null) { return null; } Transform bestTmp = itemList[0]; for (int i = 0; i < itemList.Count; i += scrollColumn) { bool result = isMax ? bestTmp.position.y < itemList[i].position.y : bestTmp.position.y > itemList[i].position.y; if (result) { bestTmp = itemList[i]; } } // Debug.Log(bestTmp.name); return bestTmp; } /// <summary> /// 查询最值同时把index传出去 /// </summary> /// <param name="isMax"></param> /// <param name="index"></param> /// <returns></returns> public Transform GetBestValueItem(bool isMax,out int index) { var tmp = GetBestValueItem(isMax); index = tmp.GetComponent<ItemPropoty>().mDRect.itemIndex; return tmp; } private List<ItemPropoty> waitChangeList; /// <summary> /// 查找到元素Y轴位置最小的元素 /// </summary> /// <param name="item"></param> private void ExecuteChangeLocation(Transform item,bool isDown) { Vector3 minItemPosition = GetBestValueItem(!isDown).position; //开始收集同排的元素位置 for (int i = 0; i < itemList.Count; i++) { if (itemList[i].Comparer_Position_Y(item.position)) { waitChangeList.SortAdd(itemList[i].GetComponent<ItemPropoty>()); } } ChangeLineLocation(isDown); } /// <summary> /// 根据刷新方式做出更改位置操作 /// </summary> /// <param name="isDown"></param> private void ChangeLineLocation(bool isDown) { if (waitChangeList == null || waitChangeList.Count == 0) { Debug.LogError("翻车了: ChangeLineLocation 查询失败"); return; } //拿到当前最低/高位置的格子 var bestValue = GetBestValueItem(!isDown).GetComponent<ItemPropoty>(); #region /* int listIndex,itemIndex; for (int i = 0; i < waitChangeList.Count; i++) { if (isDown) { itemIndex = bestValue.mDRect.itemIndex + i + scrollColumn; listIndex = i; } else { itemIndex = bestValue.mDRect.itemIndex - (waitChangeList.Count - 1); listIndex = scrollColumn - (waitChangeList.Count - i); } if (itemRectDic.ContainsKey(itemIndex)) { //当往下显示的时候将上边隐藏部位的格子释放掉 waitChangeList[listIndex].mDRect.isUse = false; //查询到左下角的首元素加上列数个单位之后 正好是下一行的位置 waitChangeList[listIndex].mDRect = itemRectDic[itemIndex]; waitChangeList[listIndex].Execute_PlacePos(); //标记新的占用位置 waitChangeList[listIndex].mDRect.isUse = true; } } */ #endregion if (isDown) { //Debug.Log("发现最底面的格子 : "+bestValue.name); for (int i = 0; i < waitChangeList.Count; i++) { //当再次刷新出现越界的时候 就是画布底面已经没有位置了不需要UI再往下刷新了 跳出去 if (itemRectDic.ContainsKey(bestValue.mDRect.itemIndex + i + scrollColumn)) { //当往下显示的时候将上边隐藏部位的格子释放掉 waitChangeList[i].mDRect.isUse = false; //查询到左下角的首元素加上列数个单位之后 正好是下一行的位置 waitChangeList[i].mDRect = itemRectDic[bestValue.mDRect.itemIndex + i + scrollColumn]; waitChangeList[i].Execute_PlacePos(); //标记新的占用位置 waitChangeList[i].mDRect.isUse = true; } } } else { for (int i = scrollColumn; i > 0 ; i--) { //Debug.Log("获取的值"+(bestValue.mDRect.itemIndex - i)); //当再次刷新出现越界的时候 就是画布底面已经没有位置了不需要UI再往下刷新了 跳出去 if (itemRectDic.ContainsKey(bestValue.mDRect.itemIndex - i)) { //Debug.Log("拾取格子"+(bestValue.mDRect.itemIndex - i)); //当往下显示的时候将上边隐藏部位的格子释放掉 waitChangeList[scrollColumn -i].mDRect.isUse = false; //查询到左下角的首元素加上列数个单位之后 正好是下一行的位置 waitChangeList[scrollColumn - i].mDRect = itemRectDic[bestValue.mDRect.itemIndex -i]; waitChangeList[scrollColumn - i].Execute_PlacePos(); //标记新的占用位置 waitChangeList[scrollColumn - i].mDRect.isUse = true; } } } //改完格子清理表 waitChangeList.Clear(); } //TODO fix /// <summary> /// 检测元素是否越界 目前的策略 用每排第一个元素进行Y轴判断移动位置 /// 所有的元素位置都保存在字典里 找出越界的位置的刷新那一排的位置 /// 判断元素向上越界还是向下越界 /// </summary> /// <returns></returns> private Transform CheckCrossTheBorder(bool isDown) { //Debug.Log("我传入的值 : " + isDown); var tmpMaxItem = GetBestValueItem(isDown); //Debug.Log(tmpMaxItem.position.y+" " + mDisplayRect.position.y); if(isDown) return tmpMaxItem.position.y > mDisplayRect.position.y ? tmpMaxItem : null; float pageBottom = mDisplayRect.position.y - mDisplayRect.sizeDelta.y; //Debug.Log("查询最小的位置 : "+pageBottom +" "+ mDisplayRect.position.y); return tmpMaxItem.position.y < pageBottom ? tmpMaxItem : null; } //查看当前的滑动方向 private float previousPagePos; private bool? isSlideDown; // 当isSlideDown为空是表明当前无操作 public bool? IsSlideDown { get => isSlideDown;} /// <summary> /// 查看当前滑动的方向 如果页面向上滑动那么Y轴是增加的 反之下滑 如果为空那么就是未参与滑动操作 /// </summary> /// <returns></returns> private void CheckCurDirection() { if (transform.position.y == previousPagePos) { isSlideDown = null; return; } isSlideDown = transform.position.y < previousPagePos; previousPagePos = transform.position.y; } private void Update() { CheckCurDirection(); if (isSlideDown == null) return; bool isTrue = (bool)isSlideDown; var changeItem = CheckCrossTheBorder(!isTrue); if (changeItem != null) ExecuteChangeLocation(changeItem, !isTrue); //TODO FIX 这里需要严重优化 暂时没考虑好具体方法 if (!isTrue) { //Debug.Log("内容框底部点 : "+(transform.position.y-(transform as RectTransform).sizeDelta.y)); //Debug.Log("显示框底部点 : "+(mDisplayRect.position.y-(mDisplayRect as RectTransform).sizeDelta.y)); var contentPos = (transform.position.y - (transform as RectTransform).sizeDelta.y); var displayPos = (mDisplayRect.position.y - (mDisplayRect as RectTransform).sizeDelta.y); //TODO FIX 临时测试代码 if (contentPos - displayPos > 80) { if (isLoad) { transform.parent.parent.GetChild(1).GetComponent<loadState>().SetLoadBar(true); StartCoroutine(LoadItem(20)); isLoad = false; } } } } //-----------------------------------------加载模块拓展---------------------------------------- private bool isLoad =true; //是否结束异步等待 public bool isAsyncWait = true; private IEnumerator LoadItem(int count) { while (isAsyncWait) { yield return new WaitForSeconds(0.3f); } //TODO FIX 测试用 AddItemPosition(count); if (allItemCount > 80) isLoad = false; isAsyncWait = true; } //TODO FIX 测试用 } internal static class ExetenceMethod_YC { //允许三个单位的误差 public static bool Comparer_Position_Y(this Transform my,Vector3 other) { if (my.position.y > other.y - 3 && my.position.y < other.y + 3) return true; return false; } public static void TryValueGet<TKey,TValue>(this Dictionary<TKey, TValue> my, TKey key, out TValue value) { if (!my.ContainsKey(key)) { Debug.LogError("键为空检测触发处 : Key = "+key); value = default(TValue); return; } my.TryGetValue(key,out value); } public static void SortAdd<T>(this IList<T> list,T TObj,bool isDescending = false) { if (typeof(T) == typeof(ItemPropoty)) { if (list.Count == 0) { list.Add(TObj); return; } int insertIndex = isDescending ? 0 : list.Count; if ((TObj as ItemPropoty).mDRect.itemIndex < (list[0] as ItemPropoty).mDRect.itemIndex) { list.Insert(list.Count - insertIndex, TObj); } else { list.Insert(insertIndex, TObj); } } } //关于处理间距的问题 计算时存再行列的数量误差 去掉最后一排的间距再进行计算 /* ----------------------------------------问题清单------------------------------------------------ 1,快速拖拽会发生丢失UI现象 2,计算行列自动布局显示半个UI的优化 3,拓展加载模块功能 4,优化下拉判断选项 */ }
这里还有一些存在的问题... 现在懒得动 以后改... 有哪位朋友使用的话可以联系我qq : 973407312
using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.UI; public class loadState : MonoBehaviour { public Text[] dot; public bool isLoading; public CustomScroll cs; private float nxtTime = 0; public void SetDot() { } public void SetLoadBar(bool isTrue) { isLoading = isTrue; gameObject.SetActive(true); StartCoroutine(Recover()); } public void BarFadeAway() { } private int index=0; public void whirl() { index++; index %= dot.Length; StartCoroutine(Bright()); } private IEnumerator Bright() { dot[index].gameObject.SetActive(false); yield return new WaitForSeconds(0.18f); dot[index].gameObject.SetActive(true); } private IEnumerator Recover() { yield return new WaitForSeconds(2); cs.isAsyncWait = false; gameObject.SetActive(false); isLoading = false; } void Update() { if (isLoading) { nxtTime += Time.deltaTime; if (nxtTime >= 0.2f) { whirl(); nxtTime = 0; } } } }
可以像这样 制作一个加载提示条 这里弄的有些简陋 可以自己弄个Dotween做个渐入渐出 多给几条提示信息
demo下载:链接:https://pan.baidu.com/s/1XS6wWAoaYWCA2GbgQUf4CQ
提取码:ms5r