基于UGUI的无限动态拖拽列表ScrollerView
渲染原理
Rect信息:抽象的格子数据,每个数据对应一个格子,矩形信息包括位置坐标,格子大小
Item格子:UI实例,每一个格子上挂载着唯一的一个Unit 控制渲染格子或者回收以及显示数据的变化
Data数据: 列表的每一个格子所属的数据,每一个这样的数据会对应一个Rect信息用来抽象出在列表中应有的格子位置
渲染流程:在每一动画帧后渲染列表。首先将蒙版坐标通过计算转化成和Item同一坐标系坐标,这样可以判断Item是否与蒙版交互,然后提取出相交的Rect信息,回收没有相交的Item格子(清除格子信息并隐藏)再将交互但还没有渲染出Item的Rect信息进行渲染,找到已回收的item渲染到rect 位置上并设置Data数据。
一、封装一个item
1.封装Rect 保存格子位置、大小
(1)Rect 用来保存这个动态格子的物理信息
(2)格子索引用来标记格子
(3)这个脚本作用是用来判断格子是否超出蒙版范围
/// <summary>
/// rect数据
/// </summary>
public class DynamicRectangle
{
/// <summary>
/// 矩形数据
/// </summary>
private Rect mRect;
/// <summary>
/// 格子索引
/// </summary>
public int Index;
/// <summary>
/// 生成2d矩形实例
/// </summary>
/// <param name="x">测量rect的X值。</param>
/// <param name="y">测量rect的Y值。</param>
/// <param name="width">矩形的宽度。</param>
/// <param name="height">矩形的高度。</param>
/// <param name="index">下标</param>
public DynamicRectangle(float x, float y, float width, float height, int index)
{
this.Index = index;
mRect = new Rect(x, y, width, height);
}
/// <summary>
/// 是否相交
/// </summary>
/// <param name="otherRect"></param>
/// <returns></returns>
public bool Overlaps(DynamicRectangle otherRect)
{
return mRect.Overlaps(otherRect.mRect);
}
/// <summary>
/// 是否相交
/// </summary>
/// <param name="otherRect"></param>
/// <returns></returns>
public bool Overlaps(Rect otherRect)
{
return mRect.Overlaps(otherRect);
}
public override string ToString()
{
return string.Format("index:{0},x:{1},y:{2},w:{3},h:{4}", Index, mRect.x, mRect.y, mRect.width, mRect.height);
}
}
2.封装item 用来保存数据和Rect(渲染数值)
(1)这个脚本用来控制格子实体
(2)用来装载数据用于交互和物理信息用于显示
/// <summary>
/// Item控制
/// </summary>
public class DynamicInfinityUnit : MonoBehaviour {
public delegate void OnSelect(DynamicInfinityUnit item);
public delegate void OnUpdateData(DynamicInfinityUnit item);
//选择时回调
public OnSelect OnSelectHanlder;
//更新数据时的回调
public OnUpdateData OnUpdateDataHandler;
/// <summary>
/// Rect(位置、大小)
/// </summary>
protected DynamicRectangle mDRect;
/// <summary>
/// Data
/// </summary>
protected object mData;
public DynamicRectangle DRect
{
set {
mDRect = value;
gameObject.SetActive(value != null);
}
get { return mDRect; }
}
/// <summary>
/// 设置数据
/// </summary>
/// <param name="data"></param>
public void SetData(object data)
{
if (data == null) return;
mData = data;
if (OnUpdateDataHandler != null)
{
OnUpdateDataHandler(this);
}
OnRenderer();
}
/// <summary>
/// 每次渲染时调用
/// </summary>
protected virtual void OnRenderer() {
}
public object GetData()
{
return mData;
}
public T GetData<T>()
{
return (T)mData;
}
}
二、封装一个scoller 类用来渲染item
创建list 渲染管理类 ,以下都是DynamicInfinityScrollerRender 类中的内容
该脚本挂载在ScollerRect 子节点List中 List为item 的父节点用来承载所有item
1.(创建list 渲染管理类)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 动态无限列表
/// </summary>
public class DynamicInfinityScrollerRender : MonoBehaviour
{
/// <summary>
/// 单元格尺寸(宽,高)
/// </summary>
public Vector2 CellSize;
/// <summary>
/// 单元格间隙(水平,垂直)
/// </summary>
public Vector2 SpacingSize;
/// <summary>
/// 列数
/// </summary>
public int ColumnCount;
/// <summary>
/// 单元格渲染器prefab
/// </summary>
public GameObject RenderGO;
/// <summary>
/// 渲染格子数
/// </summary>
protected int mRendererCount;
/// <summary>
/// 父节点蒙版尺寸
/// </summary>
private Vector2 mMaskSize;
/// <summary>
/// 蒙版矩形
/// </summary>
private Rect mRectMask;
protected ScrollRect mScrollRect;
/// <summary>
/// List(item根节点)
protected RectTransform mRectTransformContainer;
/// <summary>
/// Item集合
/// </summary>
protected List<DynamicInfinityUnit> mList_items;
/// <summary>
/// Rect集合
/// </summary>
private Dictionary<int, DynamicRectangle> mDict_dRect;
/// <summary>
/// Data数据集合
/// </summary>
protected Dictionary<int, object> mDict_DataProviders;
/// <summary>
/// 是否初始化
/// </summary>
protected bool mHasInited = false;
2.初始化渲染数据 数据初始 渲染初始
(1)记录下蒙版的大小和位置信息储存在Rect 中 ,用来对比Item 是否相交于蒙版 ,回收不相交的Item
(2)通过蒙版大小计算需要初始可以塞满蒙版的格子数,一般还要再多出一行或两行,因为拖动时会显示出N+1个行
(4)实例化Item存放在List 中 , 每个格子挂载DynamicInfinityUnit脚本用来控制这个格子,初始为回收状态(隐藏)
(6)更新格子位置与List 尺寸
/// <summary>
/// 初始化渲染脚本
/// </summary>
public virtual void InitRendererList(DynamicInfinityUnit.OnSelect OnSelect, DynamicInfinityUnit.OnUpdateData OnUpdateData)
{
if (mHasInited) return;
//List 承载所有Item 的父节点
mRectTransformContainer = transform as RectTransform;
//记录一下蒙版尺寸
mMaskSize = transform.parent.GetComponent<RectTransform>().sizeDelta;
mScrollRect = transform.parent.GetComponent<ScrollRect>();
//初始渲染的item 的个数 多渲染一列 通过蒙版的尺寸和格子的尺寸计算需要渲染器的个数(格子数 = 行数 * 列数)
mRendererCount = ColumnCount * (Mathf.CeilToInt(mMaskSize.y / GetBlockSizeY()) + 1);
//初始一个蒙版的Rect
_UpdateDynamicRects(mRendererCount);
//初始每一个渲染出的item 都有DynamicInfinityUnit 用来控制
mList_items = new List<DynamicInfinityUnit>();
for (int i = 0; i < mRendererCount; i++)
{
//预设体克隆实例
GameObject child = GameObject.Instantiate(RenderGO);
child.transform.SetParent(transform);
child.transform.localRotation = Quaternion.identity;
child.transform.localScale = Vector3.one;
child.layer = gameObject.layer;
RectTransform re = child.transform as RectTransform;
re.sizeDelta = CellSize;
DynamicInfinityUnit dfItem = child.GetComponent<DynamicInfinityUnit>();
if (dfItem == null)
child.AddComponent<DynamicInfinityUnit>();
mList_items.Add(dfItem);
//选择回调
mList_items[i].OnSelectHanlder = OnSelect;
//更新数据回调
mList_items[i].OnUpdateDataHandler = OnUpdateData;
child.SetActive(false);
//初始item位置
_UpdateChildTransformPos(child, i);
}
//初始该list的尺寸 遮罩尺寸
_SetListRenderSize(mRendererCount);
mHasInited = true;
}
3.设置渲染列表的尺寸 设置item父节点List 的x、y值 设置蒙版的尺寸
List 的宽度不变 长度为一列的item数*(item高+行高)
(1)计算出List 大小,用来List为所有item大小总和
(2)记录蒙版Rect,前面有说过是用来对比每个格子的是否在蒙版范围内
/// <summary>
/// 设置渲染列表的尺寸
/// </summary>
/// <param name="count">渲染格子个数</param>
private void _SetListRenderSize(int count)
{
//赋值list 的大小 x轴保持不变 Y轴获取 (行数 * 行高)
mRectTransformContainer.sizeDelta = new Vector2(mRectTransformContainer.sizeDelta.x, Mathf.CeilToInt((count * 1.0f / ColumnCount)) * GetBlockSizeY());
//记录蒙版的尺寸 蒙版尺寸为父节点的尺寸
mRectMask = new Rect(0, -mMaskSize.y, mMaskSize.x, mMaskSize.y);
//是否需要拖动条
mScrollRect.vertical = mRectTransformContainer.sizeDelta.y > mMaskSize.y;
}
4.每个格子的大小,行距
/// <summary>
/// 获取格子块尺寸
/// </summary>
/// <returns></returns>
protected float GetBlockSizeY() {
return CellSize.y + SpacingSize.y;
}
protected float GetBlockSizeX()
{
return CellSize.x + SpacingSize.x;
}
5.对Item 的操作
(1)封装更新Item 的方法
根据Index 计算出Item 应该对应于List 的位置坐标,
Item和List的锚点一点要调整:左下角为格子的锚点 List的左上角为锚点
举例:第三个格子的位置
(2)回收所有Item 当Item 不再显示了 , 这时我们应该回收掉以供接下来使用
置空 DynamicInfinityUnit 的DRect 就可以隐藏掉Item
(3)获取已经回收的格子
在DynamicInfinityUnit 集合中找到DRect被置空的格子
(4)通过Rect 获取格子 每一个DRect都有一个唯一的Index 对应与数据的唯一ID
#region Item操作
/// <summary>
/// 更新各个渲染格子的位置
/// </summary>
/// <param name="child"></param>
/// <param name="index"></param>
private void _UpdateChildTransformPos(GameObject child, int index)
{
//根据坐标判断所在行、列 再计算xy坐标
int row = index / ColumnCount;
int column = index % ColumnCount;
Vector2 v2Pos = new Vector2();
v2Pos.x = column * GetBlockSizeX();
v2Pos.y = -CellSize.y - row * GetBlockSizeY();
((RectTransform)child.transform).anchoredPosition3D = Vector3.zero;
((RectTransform)child.transform).anchoredPosition = v2Pos;
}
/// <summary>
/// 清理所有已经存在的格子的矩形信息(回收所有格子)
/// </summary>
private void _ClearAllListRenderDr()
{
if (mList_items != null)
{
for (int i = 0; i < mList_items.Count; i++)
{
DynamicInfinityUnit item = mList_items[i];
item.DRect = null;
}
}
}
/// <summary>
/// 获取已经回收的格子
/// </summary>
/// <returns></returns>
private DynamicInfinityUnit _GetNullDynamicItem()
{
for (int i = 0; i < mList_items.Count; i++)
{
DynamicInfinityUnit item = mList_items[i];
if (item.DRect == null)
return item;
}
return null;
}
/// <summary>
/// 通过rect获得格子
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
DynamicInfinityUnit _GetDynamicItem(DynamicRectangle rect)
{
for (int i = 0; i < mList_items.Count; i++)
{
DynamicInfinityUnit item = mList_items[i];
if (item.DRect == null)
continue;
if (rect.Index == item.DRect.Index)
return item;
}
return null;
}
#endregion
6.Rects 矩形数据集合操作
(1)更新对应于数据的rect 一个数据对应一个rect数据
(2)每添加一个rect 或者删除一个rect 都是对列表末尾操作
#region Rects 矩形数据集合操作
/// <summary>
/// 更新对应于数据的rect
/// </summary>
private void _UpdateDynamicRects(int count)
{
//生成渲染用的格子
mDict_dRect = new Dictionary<int, DynamicRectangle>();
for (int i = 0; i < count; i++)
{
//行
int row = i / ColumnCount;
//列
int column = i % ColumnCount;
//记录矩形的宽高、位置
//父节点的锚点为左上角 item的锚点为左下角
DynamicRectangle dRect = new DynamicRectangle(column * GetBlockSizeX(), -row * GetBlockSizeY() - CellSize.y, CellSize.x, CellSize.y, i);
mDict_dRect[i] = dRect;
}
}
/// <summary>
/// 添加一个渲染格子
/// </summary>
/// <param name="index"></param>
private void _AddDynamicRect()
{
if (mDict_dRect == null)
mDict_dRect = new Dictionary<int, DynamicRectangle>();
int index = mDict_dRect.Count;
int row = index / ColumnCount;
int column = index % ColumnCount;
DynamicRectangle dRect = new DynamicRectangle(column * GetBlockSizeX(), -row * GetBlockSizeY() - CellSize.y, CellSize.x, CellSize.y, index);
mDict_dRect[index] = dRect;
}
/// <summary>
/// 删除全部Rect
/// </summary>
private void _CloseAllDynamicRect()
{
mDict_dRect = null;
}
/// <summary>
/// 移除一个Rect
/// </summary>
private void _CloseDynamicRect()
{
mDict_dRect.Remove(mDict_dRect.Count);
}
#endregion
7.对数据Data 的操作
(1)数据列表中存放所有数据 Data
(2)每个数据Data都会对应一个rect
(3)对数据的增删改查最后都会作用于rect集合
#region Data 操作
/// <summary>
/// 添加所有的数据
/// </summary>
/// <param name="strings"></param>
private void _AddDynamicDatas(List<object> strings)
{
mDict_DataProviders = new Dictionary<int, object>();
for (int i = 0; i < strings.Count; i++)
{
mDict_DataProviders.Add(i, strings[i]);
}
}
/// <summary>
/// 添加一个数据
/// </summary>
/// <param name="str"></param>
private void _AddDynamicData(object str)
{
if (mDict_DataProviders == null)
mDict_DataProviders = new Dictionary<int, object>();
mDict_DataProviders.Add(mDict_DataProviders.Count, str);
}
/// <summary>
/// 移除一个数据
/// </summary>
/// <param name="index"></param>
private void _RemoveDynamicData(int index)
{
if (mDict_DataProviders == null)
return;
mDict_DataProviders.Remove(index);
}
private void _ClearDynamicDatas()
{
if (mDict_DataProviders == null)
return;
mDict_DataProviders.Clear();
}
private void _UpdateDynamicData(int index, object str)
{
if (mDict_DataProviders == null)
return;
if (mDict_DataProviders.ContainsKey(index))
{
mDict_DataProviders[index] = str;
}
}
#endregion
8.移动到目标行
(1)控制目标行的范围 , 不要超过最后X位下标 (X为蒙版内可显示出的行数)
(2)求出坐标和行数 (注意拖动的是List,List 坐标在改变Item根据是否与蒙版相交而显示) 目标x轴保持不动就行 , y轴为 移动的(行数+行间距)*格子数
举例: 一格就是初始状态 List 坐标(0,0) 四格就是 List坐标为 (0,3X(格子高+行距))
(3)之后就用协程执行, 采用Lerp差值实现平滑移动到目标位置
#region 移动至目标
/// <summary>
/// 移动列表使之能定位到给定数据的位置上
/// </summary>
/// <param name="target">目标</param>
/// <param name="delay">延迟</param>
public virtual void LocateRenderItemAtTarget(object target, float delay)
{
if (mDict_DataProviders.ContainsValue(target))
{
foreach (KeyValuePair<int, object> kvp in mDict_DataProviders)
{
if (kvp.Value.Equals(target))
{
//找到目标对象的下标
LocateRenderItemAtIndex(kvp.Key, delay);
return;
}
}
}
}
/// <summary>
/// 根据目标下标判断移动目的地
/// </summary>
/// <param name="index"></param>
/// <param name="delay"></param>
public virtual void LocateRenderItemAtIndex(int index, float delay)
{
//查看是否在超范围
if (index < 0 || index > mDict_DataProviders.Count - 1)
throw new Exception("Locate Index Error " + index);
index = Math.Min(index, mDict_DataProviders.Count - mRendererCount);
index = Math.Max(0, index);
//List 的坐标
Vector2 pos = mRectTransformContainer.anchoredPosition;
//目标的行数
int row = index / ColumnCount;
//目的地的坐标
Vector2 v2Pos = new Vector2(pos.x, row * GetBlockSizeY());
m_Coroutine = StartCoroutine(TweenMoveToPos(pos, v2Pos, delay));
}
protected Coroutine m_Coroutine = null;
/// <summary>
/// 移动协程
/// </summary>
/// <param name="pos">现在的坐标</param>
/// <param name="v2Pos">目的地坐标</param>
/// <param name="delay">延迟</param>
/// <returns></returns>
protected IEnumerator TweenMoveToPos(Vector2 pos, Vector2 v2Pos, float delay)
{
bool running = true;
//延迟时间
float passedTime = 0f;
while (running)
{
yield return new WaitForEndOfFrame();
passedTime += Time.deltaTime;
Vector2 vCur;
//到达设置延迟时间的点
if (passedTime >= delay)
{
//遮罩xy
vCur = v2Pos;
running = false;
StopCoroutine(m_Coroutine);
m_Coroutine = null;
}
else
{
//差值移动
vCur = Vector2.Lerp(pos, v2Pos, passedTime / delay);
}
mRectTransformContainer.anchoredPosition = vCur;
}
}
private void _MoveToPos(int index)
{
//查看是否在超范围
if (index < 0 || index > mDict_DataProviders.Count - 1)
throw new Exception("Locate Index Error " + index);
index = Math.Min(index, mDict_DataProviders.Count - mRendererCount);
index = Math.Max(0, index);
//List 的坐标
Vector2 pos = mRectTransformContainer.anchoredPosition;
//目标的行数
int row = index / ColumnCount;
//目的地的坐标
Vector2 v2Pos = new Vector2(pos.x, row * GetBlockSizeY());
mRectTransformContainer.anchoredPosition = v2Pos;
}
#endregion
9.接下来的重点就是更新渲染
(1)首先求出来蒙版的rect 原点为左下角 , 参照坐标系父节点为List和Item一样
(2)遍历所有的Rect信息 , 每个数据都有对应的Rect信息储存着item应该显示的位置 判断是否与蒙版相交 如果相交保存一个相交合集
(3)利用相交合集 判断item是否需要回收
(4)将新进入蒙版需要显示的数据 分配一个已回收的item 设置其位置并显示
private void Update()
{
if (mHasInited)
{
UpdateRender();
}
}
/// <summary>
/// 更新渲染
/// </summary>
protected void UpdateRender()
{
//蒙版y = - 蒙版高 - List坐标y 获取的是蒙版相对于List的坐标
mRectMask.y = -mMaskSize.y - mRectTransformContainer.anchoredPosition.y;
Dictionary<int, DynamicRectangle> inOverlaps = new Dictionary<int, DynamicRectangle>();
foreach (DynamicRectangle dR in mDict_dRect.Values)
{
if (dR.Overlaps(mRectMask))
{
//如果与蒙版相交
inOverlaps.Add(dR.Index, dR);
}
}
for (int i = 0; i < mList_items.Count; i++)
{
//查看格子是否蒙版相交如果没有相交则消除
DynamicInfinityUnit item = mList_items[i];
if (item.DRect != null && !inOverlaps.ContainsKey(item.DRect.Index))
item.DRect = null;
}
foreach (DynamicRectangle dR in inOverlaps.Values)
{
if (mDict_DataProviders != null && dR.Index < mDict_DataProviders.Count)
{
if (_GetDynamicItem(dR) == null)
{
DynamicInfinityUnit item = _GetNullDynamicItem();
item.DRect = dR;
_UpdateChildTransformPos(item.gameObject, dR.Index);
item.SetData(mDict_DataProviders[dR.Index]);
}
}
}
}
三、拖动列表的增、删、改、查
接下来就可以实现增删改查了,其实增删用到的比较多而且可以根据需求灵活修改对应的方式
基本逻辑就是:添加数据保证每一个数据Data对应一个rect , 而List 大小可以装载所有rect 的尺寸 , 而item进行实际的可见渲染。
#region 增删改查
/// <summary>
/// 添加数据集合
/// </summary>
/// <param name="list"></param>
public void AddDatasToInfinityList(List<object> list)
{
//配置的rect
_UpdateDynamicRects(list.Count);
//设置List大小
_SetListRenderSize(list.Count);
//配置data
_AddDynamicDatas(list);
//回收所有item
_ClearAllListRenderDr();
}
/// <summary>
/// 添加一个数据
/// </summary>
/// <param name="obj"></param>
public void AddDataToInfinityList(object obj)
{
//添加数据至末端
_AddDynamicData(obj);
//添加rect在末端
_AddDynamicRect();
//设置List大小
_SetListRenderSize(mDict_DataProviders.Count);
}
/// <summary>
/// 移除整个List
/// </summary>
public void RemoveAllDatasToInfinityList()
{
//移动至第一行
_MoveToPos(0);
//回收所有的item
_ClearAllListRenderDr();
//置空数据
_ClearDynamicDatas();
//置空rect
_CloseAllDynamicRect();
//缩短List
_SetListRenderSize(0);
}
/// <summary>
/// 更改数据
/// </summary>
/// <param name="index"></param>
/// <param name="str"></param>
public void UpdateDataToInfinityList(int index, object str)
{
_UpdateDynamicData(index, str);
//清除所有格子信息
_ClearAllListRenderDr();
}
/// <summary>
/// 移除一个item
/// </summary>
public void RemoveDataToInfinityList(int index)
{
//清除所有已存在的格子的矩形信息
_ClearAllListRenderDr();
_RemoveDynamicData(index);
_SetListRenderSize(mDict_DataProviders.Count);
_UpdateDynamicRects(mDict_DataProviders.Count);
}
#endregion
到这里这个DynamicInfinityScrollerRender 类可以封装起来了。
四、总结
需要注意的几个点是:
(1)这个拖拽列表在每次拖拽到新的一行时,需要将回收的item重新渲染出来所以如果数据最好缓存起来以提高效率.
比如:如果你在赋值item数据的时候加载图片 ,那就会在每次拖动时新的item就会被赋值再加载图片 每次就会卡顿,
如果你在一开始就加载好图片再添加数据的时候赋值引用,那么每次拖动时新的item就不用再去加载图片了
(2)锚点和中心点记得要调整,当然也可以根据自己的需求修改。
这个也是参考其他大神的一些例子改出来的,借用了很多别人的东西,一开始没有注释也看不太懂,自己搞半天慢慢改了改,就分享了出来,这个无限动态拖拽列表基于UGUI,可能有些需要不太合适,后续还会分享出其他的动态拖拽列表和一些UI知识,也希望有些不足或者错的地方能有大佬指出来一起探讨一起进步。
链接: 测试工程.
提取码:z1bk
看了这么久别忘了收藏一下呀!
链接: https://blog.csdn.net/weixin_40492525/article/details/102708015.