Unity实用小工具或脚本——可折叠伸缩的多级(至少三级)内容列表(类似于Unity的Hierarchy视图中的折叠效果)
Posted 凯尔八阿哥
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity实用小工具或脚本——可折叠伸缩的多级(至少三级)内容列表(类似于Unity的Hierarchy视图中的折叠效果)相关的知识,希望对你有一定的参考价值。
目录
一、前言
首先,介绍下开发环境,我使用的是Unity2019.4.26。文章末尾有工程文件,如果不想看过程的可以直接拉到最后。效果图如图1所示:该工具主要的亮点是可以自动实现多级菜单展示,目前测试的是三级,如果想实现更多层级只需要在代码里增加多级枚举和处理枚举的逻辑代码,基本上实现的思路是一样的。这个效果有点类似于Unity的Hierarchy视图中的折叠效果,如图2所示,实现这个效果最核心的是要理解Unity的UGUI的ScrollRect控件,这个控件的功能远比我想象的要大,另外还需要了解UGUI的
锚点的概念,比如,如图1所示的效果,如果某个菜单项下面有子菜单项目,点击该菜单项的展开按钮。在计算展开的高度的同时是需要对该项进行移动的,移动的距离就和子项展开后的总高度直接相关。如果该项的子项目能继续展开,除了要处理子项目的展开逻辑,正如前面描述的那样,展开后得到该项目的孙子项目;还需要处理该项目的孙子项目展开后对该项目的影响,孙子项目展开后的高度也是该项目的移动距离。类似于这样层层相关,环环相扣,这也是这个实现逻辑里最难处理的部分。做这样一个内容列表,首先当然要有内容了,我这里直接在Unity的Hierarchy里创建了如图2所示的内容菜单项,然后,去读取该内容项,再创建属于我的UI内容项。读取内容的时候就给每个项目做好层级划分,然后实例化预设体,该预设体即UI项的模板。再该项目中我只用了一个模板来生成不同的层级项。好了,废话BB完了,直接开撸吧。
二、实现
2.1、创建ScrollView
创建一个ScrollView,只保留它有垂直的滚动条;然后在Cotent中的设置如图3所示:增加ContentSizeFitter和VerticalLayoutGroup组件,并设置VerticalLayoutGroup的Padding。
2.2、制作层级预设体BaseLevelPartObj
该预设体中最重要的就是要有和2.1中的ScollView一样的Content子物体,其设置如图4所示:设置其Padding为向左偏移,这样就能保证其子物体会在其下向右偏移的位置。
2.3、设置该预设体的初始化处理方法
这一步要首先将该项的初始位置保存起来,这个初始位置就可以作为最后折叠时该项目的位置,无论该项目的子孙项展开到了什么程度。然后是设置其按钮的显示状态。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 层级菜单项
/// </summary>
public class BaseLevelPartObj : MonoBehaviour
public static BaseLevelPartObj CurSelectedBLPO;
public Text TextTitle;//标题
public Image ImageBtnBg;//背景按钮
public Image[] ImageBtnFold;//折叠按钮0时折叠,1是展开
public RectTransform M_MySelfRectT get; private set;
public ContentSizeFitter M_SubObjParentObj get; private set; //下级菜单的父物体
public BaseLevelPartObj M_MyParetnObj get; private set; //我的父物体
public PartsLevel M_Level get; private set; //我的层级
public float M_HeightAllSubItems get; private set; = 0;//整个子列表的加起来的高度,包括所有的子孙项
public bool M_IsBeSelected get;set;
private Vector2 PriFloldSizeDelta;//折叠时候的位置
public bool M_IsFold get; private set; = true;//是否折叠
private static float TimeLastClick;//上次点中的时间
private void SetLevelShow()
switch (M_Level)
case PartsLevel.First:
//如果是一级菜单则其父物体为最上面的内容管理器
transform.SetParent(UIManger.M_Instance.ParentPartFirstLevel.transform);
break;
case PartsLevel.Second:
break;
case PartsLevel.Third:
break;
if (null != M_MyParetnObj)
transform.SetParent(M_MyParetnObj.M_SubObjParentObj.transform);
// M_MyParetnObj.M_SubObjParentObj.gameObject.SetActive(false); 一开始除了一级菜单都不显示
public virtual void Init(string partName,BaseLevelPartObj parent, PartsLevel level)
ImageBtnBg.color = UIManger.M_Instance.ColorBtnSelf[0];//初始化背景图
M_SubObjParentObj = GetComponentInChildren<ContentSizeFitter>(true);
M_MySelfRectT = GetComponent<RectTransform>();
PriFloldSizeDelta = M_MySelfRectT.sizeDelta;//保存初始位置
M_MyParetnObj = parent;
M_Level = level;
TextTitle.text = partName;
SetLevelShow();
ImageBtnFold[0].gameObject.SetActive(false);
ImageBtnFold[1].gameObject.SetActive(false);
if (null != M_MyParetnObj)
M_MyParetnObj.M_SubObjParentObj.gameObject.SetActive(false); 一开始除了一级菜单都不显示
M_MyParetnObj.AddAllSubItemHeight(M_MySelfRectT.rect.height);//增加当前物体的父物体子列表高度
2.4、读取Hierarchy的内容并创建UI项
读取的时候采用了.GetComponentsInChildren<Transform>方法,将目标物体的所有子物体都读取进来,当然也包括它自己,
Transform[] tempTransObj = ReadObj.GetComponentsInChildren<Transform>(true);
然后采用一次遍历的方式创建所有的实体,并且区分开每个读取项的层级。这里有个非常重要的知识点需要掌握,通过GetComponentsInChildren<Transform>读取出来的数组其顺序时有规律的,前面的项是穷尽读取完每个层级之后才会去读下个层级的项。例如,如图5所示,读取的tempTransObj数组的前面几项分别是0:GameObjects、1:一级菜单1、2:二级菜单101、3:三级菜单10101、.....18:二级菜单101 (1)。也即,一定是在一级菜单1中穷尽完“二级菜单101”的所有子项后,才会进入“二级菜单101 (1)”的读取,完美的实现了从上之下的索引顺序。
鉴于此规律,我们就可以采用一次遍历的方式来实现每个项的初始化,也即每个子项目在创建的时候它的父项是肯定已经出现并创建的。基于这个规律我们在遍历的时候就可以存储那些临时的一级、二级、菜单项,等到下个项被遍历的时候先判断其父物体是不是已经存在了,然后直接将其父物体设置成可以展开的姿态。当然,如果存储的一级、二级项,如果一直没有子物体就会保持默认的没有折叠或展开的按钮。
在遍历之前我们还需要一个判断当前读取的Transform是在第几个层级的方法,代码如下:采用递归读取的方式穷尽其父物体,然后得出其所在层级的数字,第几级就为数字几。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIManger : MonoBehaviour
public BaseLevelPartObj PrefabsPartObj;
public Transform ReadObj;
public Color[] ColorBtnSelf;
public static UIManger M_Instance
get
if (null == instance)
instance = FindObjectOfType<UIManger>();
return instance;
public ContentSizeFitter ParentPartFirstLevel;//一级菜单的父物体
private List<BaseLevelPartObj> ListAllPartObjs = new List<BaseLevelPartObj>();
private static UIManger instance;
// Start is called before the first frame update
void Start()
//先缓存一级和二级的菜单项
Dictionary<string, BaseLevelPartObj> tempDicFirstLevelObj = new Dictionary<string, BaseLevelPartObj>();
Dictionary<string, BaseLevelPartObj> tempDicSecLevelObj = new Dictionary<string, BaseLevelPartObj>();
Transform[] tempTransObj = ReadObj.GetComponentsInChildren<Transform>(true);
//采用一次遍历创建完所有的层级菜单项
foreach (Transform item in tempTransObj)
BaseLevelPartObj tempBLPObj = Instantiate(PrefabsPartObj);
PartsLevel tempLevel = (PartsLevel)(GetLevel(item) - 1);
// Debug.Log(item.name + ":" + GetLevel(item) + ":p:"+(item.parent==null?"null":item.parent.name));
switch (tempLevel)
case PartsLevel.None:
break;
case PartsLevel.First:
tempBLPObj.Init(item.name, null, tempLevel);
tempDicFirstLevelObj.Add(item.name, tempBLPObj);
break;
case PartsLevel.Second:
//如果临时一级菜单中有包含当前物体的父物体
if (tempDicFirstLevelObj.ContainsKey(item.parent.name))
BaseLevelPartObj tempParentObj = tempDicFirstLevelObj[item.parent.name];
tempParentObj.HasSubObj();
tempBLPObj.Init(item.name, tempParentObj, tempLevel);
tempDicSecLevelObj.Add(item.name, tempBLPObj);
break;
case PartsLevel.Third:
//如果临时的二级菜单中有包含当前物体的父物体
if (tempDicSecLevelObj.ContainsKey(item.parent.name))
BaseLevelPartObj tempParentObj = tempDicSecLevelObj[item.parent.name];
tempParentObj.HasSubObj();
tempBLPObj.Init(item.name, tempParentObj, tempLevel);
break;
// Update is called once per frame
void Update()
//获取当前的层级
public int GetLevel(Transform transfor)
int tempLevel = 1;
if(transfor.parent!=null)
return tempLevel+ GetLevel(transfor.parent);
return tempLevel;
public enum PartsLevel
None = 0,
First = 1,
Second,
Third,
读取层级之后,需要转换成枚举,因为,读取的Transform数组的第一个不作为层级,因此减去1就可以等到所在的层级了。
2.5、折叠与展开
1)自身展开和折叠逻辑
完成了上述几个步骤之后,我们就可以生成包含所有层级的UI项了,接下来就是最重要的步骤,怎么折叠和展开。在BaseLevelPartObj的初始化函数里就已经使用了增加子项目高度的方法,其实现内容为:就一句话
public void AddAllSubItemHeight(float height)
M_HeightAllSubItems += height;
其调用的代码为:在其父物体不为空的时候,给其父物体增加子项目高度,注意这里还只是处理了一级子项目的高度,还未处理二级子项目展开时候的高度,这个后面再讲。
if (null != M_MyParetnObj)
M_MyParetnObj.M_SubObjParentObj.gameObject.SetActive(false); 一开始除了一级菜单都不显示
M_MyParetnObj.AddAllSubItemHeight(M_MySelfRectT.rect.height);//增加当前物体的父物体子列表高度
然后是处理展开按钮的逻辑代码:这里有个很有意思的处理过程,也即将存储该项子项目的ContentSizeFitter这个组件需要先失活,然后再位置都变动好了之后再激活,这么操作的目的是为了刷新。我也是很佩服我自己居然找出了这样的奇葩刷新方式,也是
/// <summary>
/// 是否折叠子列表
/// </summary>
/// <param name="isFold"></param>
public void Btn_FoldSubList(bool isFold)
M_IsFold = isFold;
ContentSizeFitter tempSubItemsParent = M_MyParetnObj == null ? UIManger.M_Instance.ParentPartFirstLevel : M_MyParetnObj.M_SubObjParentObj;
tempSubItemsParent.enabled = false;
M_SubObjParentObj.gameObject.SetActive(!isFold);
SetFoldImage(isFold);
float tempSign = isFold ? -1 : 1;
if (isFold)
M_MySelfRectT.sizeDelta = PriFloldSizeDelta;
else
M_MySelfRectT.sizeDelta = PriFloldSizeDelta + new Vector2(0, M_HeightAllSubItems);
tempSubItemsParent.enabled = true;
基于一次偶然的操作才发现,不知道这算不算Unity的问题还是我没找到更好的处理方法的问题。为了说明这个问题,我做一个简单的测试你就会明白了,我就先将处理这个ContentSizeFitter组件失活、激活的代码注释掉,然后运行我的工程文件,你就发现效果是这样的,如图6所示,在展开一级菜单的时候,明显发生了错位的现象,然后我手动的失活、激活一级菜单的父物体上的ContentSizeFitter组件后就会让一级菜单回归到展开后应该的位置处。这个奇葩刷新过程,真的是让人很无语,我尝试过使用
Canvas.ForceUpdateCanvases();
方法也不行,只好先这么着吧,处理的时候会相对来说麻烦一点。
2)子项展开和折叠逻辑
上面处理了自身展开后的逻辑,但是,子项目展开后的效果是这样的,如图7所示:子项目展开后显示了一堆的孙子项目,孙子项目的高度直接超过了该项当前的高度,所以就溢出到了下面的层级中,这个肯定不是我们想要的结果,为此需要专门处理
子菜单展开后该项目的位置和高度关系以及ContentSizeFitter的失活和激活带来的刷新。新增子项目展开带来的高度处理逻辑,然后在折叠展开按钮的方法中添加调用方法。记住一定要处理展开和折叠带来的高度差数值符号的变换,展开是正的,折叠是负号
/// <summary>
/// 子菜单展开带来的高度增加
/// </summary>
public void SubItemFoldAddHeight(float height)
AddAllSubItemHeight(height);
ContentSizeFitter tempSubItemsParent = M_MyParetnObj == null ? UIManger.M_Instance.ParentPartFirstLevel : M_MyParetnObj.M_SubObjParentObj;
//如果是一级列表则是最上面的那个父物体
tempSubItemsParent.enabled = false;
M_MySelfRectT.sizeDelta =PriFloldSizeDelta+ new Vector2(0, M_HeightAllSubItems);
tempSubItemsParent.enabled = true;
/// <summary>
/// 是否折叠子列表
/// </summary>
/// <param name="isFold"></param>
public void Btn_FoldSubList(bool isFold)
....
//给父物体也增加高度并且刷新
if (null != M_MyParetnObj)
M_MyParetnObj.SubItemFoldAddHeight(tempSign*M_HeightAllSubItems);
2.6、完整的两个代码
层级预设体再加上处理鼠标悬浮在其上的背景变化和按下时候的颜色变化逻辑后完整的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 层级菜单项
/// </summary>
public class BaseLevelPartObj : MonoBehaviour
public static BaseLevelPartObj CurSelectedBLPO;
public Text TextTitle;//标题
public Image ImageBtnBg;//背景按钮
public Image[] ImageBtnFold;//折叠按钮0时折叠,1是展开
public RectTransform M_MySelfRectT get; private set;
public ContentSizeFitter M_SubObjParentObj get; private set; //下级菜单的父物体
public BaseLevelPartObj M_MyParetnObj get; private set; //我的父物体
public PartsLevel M_Level get; private set; //我的层级
public float M_HeightAllSubItems get; private set; = 0;//整个子列表的加起来的高度,包括所有的子孙项
public bool M_IsBeSelected get;set;
private Vector2 PriFloldSizeDelta;//折叠时候的位置
public bool M_IsFold get; private set; = true;//是否折叠
private static float TimeLastClick;//上次点中的时间
// Start is called before the first frame update
void Start()
// Update is called once per frame
void Update()
private void SetFoldImage(bool isFold)
if (M_Level == PartsLevel.Third) return;
ImageBtnFold[0].gameObject.SetActive(!isFold);
ImageBtnFold[1].gameObject.SetActive(isFold);
public void AddAllSubItemHeight(float height)
M_HeightAllSubItems += height;
/// <summary>
/// 是否折叠子列表
/// </summary>
/// <param name="isFold"></param>
public void Btn_FoldSubList(bool isFold)
M_IsFold = isFold;
ContentSizeFitter tempSubItemsParent = M_MyParetnObj == null ? UIManger.M_Instance.ParentPartFirstLevel : M_MyParetnObj.M_SubObjParentObj;
tempSubItemsParent.enabled = false;
M_SubObjParentObj.gameObject.SetActive(!isFold);
SetFoldImage(isFold);
float tempSign = isFold ? -1 : 1;
if (isFold)
M_MySelfRectT.sizeDelta = PriFloldSizeDelta;
else
M_MySelfRectT.sizeDelta = PriFloldSizeDelta + new Vector2(0, M_HeightAllSubItems);
tempSubItemsParent.enabled = true;
//给父物体也增加高度并且刷新
if (null != M_MyParetnObj)
M_MyParetnObj.SubItemFoldAddHeight(tempSign*M_HeightAllSubItems);
//搜索项选中的时候自动展开其所有父类
public bool SearcherSelectedUnFlod()
bool isOver = false;
if (null != M_MyParetnObj)
if (M_MyParetnObj.M_IsFold) M_MyParetnObj.Btn_FoldSubList(!M_MyParetnObj.M_IsFold);
return M_MyParetnObj.SearcherSelectedUnFlod();
if (M_IsFold) Btn_FoldSubList(!M_IsFold);
return isOver;
/// <summary>
/// 子菜单展开带来的高度增加
/// </summary>
public void SubItemFoldAddHeight(float height)
AddAllSubItemHeight(height);
ContentSizeFitter tempSubItemsParent = M_MyParetnObj == null ? UIManger.M_Instance.ParentPartFirstLevel : M_MyParetnObj.M_SubObjParentObj;
//如果是一级列表则是最上面的那个父物体
tempSubItemsParent.enabled = false;
M_MySelfRectT.sizeDelta =PriFloldSizeDelta+ new Vector2(0, M_HeightAllSubItems);
tempSubItemsParent.enabled = true;
public virtual void Btn_PointEnter()
if (M_IsBeSelected) return;
ImageBtnBg.color = UIManger.M_Instance.ColorBtnSelf[1];
public virtual void Btn_PointExit()
if (M_IsBeSelected) return;
ImageBtnBg.color = UIManger.M_Instance.ColorBtnSelf[0];
public virtual void Btn_PointDown()
if(null!= CurSelectedBLPO&& CurSelectedBLPO!=this)
CurSelectedBLPO.M_IsBeSelected = false;
CurSelectedBLPO.ImageBtnBg.color= UIManger.M_Instance.ColorBtnSelf[0];
CurSelectedBLPO = this;
// UIManger.M_Instance.SelectedBLPobj(this);
M_IsBeSelected = true;
ImageBtnBg.color = UIManger.M_Instance.ColorBtnSelf[2];
//搜索被选中
public void SearcherBeSelected()
M_IsBeSelected = true;
ImageBtnBg.color = UIManger.M_Instance.ColorBtnSelf[3];
//if (null != M_MyParetnObj)
//
// M_MyParetnObj.Btn_FoldSubList(false);
//
public virtual void Btn_PointClick()
if(M_IsBeSelected)
if(Time.time-TimeLastClick<0.2f)
// Debug.Log("点击了一次!");
TimeLastClick = Time.time;
ImageBtnBg.color = UIManger.M_Instance.ColorBtnSelf[3];
//有子物体了,设置其折叠按钮
public void HasSubObj()
SetFoldImage(true);
/// <summary>
/// 是否是搜索的时候显示
/// </summary>
/// <param name="isSercher"></param>
public void SearcherShow(bool isSercher)
gameObject.SetActive(true);
if(isSercher)
ImageBtnFold[0].gameObject.SetActive(false);
ImageBtnFold[1].gameObject.SetActive(false);
else
SetLevelShow();
private void SetLevelShow()
switch (M_Level)
case PartsLevel.First:
//如果是一级菜单则其父物体为最上面的内容管理器
transform.SetParent(UIManger.M_Instance.ParentPartFirstLevel.transform);
break;
case PartsLevel.Second:
break;
case PartsLevel.Third:
break;
if (null != M_MyParetnObj)
transform.SetParent(M_MyParetnObj.M_SubObjParentObj.transform);
// M_MyParetnObj.M_SubObjParentObj.gameObject.SetActive(false); 一开始除了一级菜单都不显示
public virtual void Init(string partName,BaseLevelPartObj parent, PartsLevel level)
ImageBtnBg.color = UIManger.M_Instance.ColorBtnSelf[0];//初始化背景图
M_SubObjParentObj = GetComponentInChildren<ContentSizeFitter>(true);
M_MySelfRectT = GetComponent<RectTransform>();
PriFloldSizeDelta = M_MySelfRectT.sizeDelta;
M_MyParetnObj = parent;
M_Level = level;
TextTitle.text = partName;
SetLevelShow();
ImageBtnFold[0].gameObject.SetActive(false);
ImageBtnFold[1].gameObject.SetActive(false);
if (null != M_MyParetnObj)
M_MyParetnObj.M_SubObjParentObj.gameObject.SetActive(false); 一开始除了一级菜单都不显示
M_MyParetnObj.AddAllSubItemHeight(M_MySelfRectT.rect.height);//增加当前物体的父物体子列表高度
读取和创建UI项的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UIManger : MonoBehaviour
public BaseLevelPartObj PrefabsPartObj;
public Transform ReadObj;
public Color[] ColorBtnSelf;
public static UIManger M_Instance
get
if (null == instance)
instance = FindObjectOfType<UIManger>();
return instance;
public ContentSizeFitter ParentPartFirstLevel;//一级菜单的父物体
private List<BaseLevelPartObj> ListAllPartObjs = new List<BaseLevelPartObj>();
private static UIManger instance;
// Start is called before the first frame update
void Start()
//先缓存一级和二级的菜单项
Dictionary<string, BaseLevelPartObj> tempDicFirstLevelObj = new Dictionary<string, BaseLevelPartObj>();
Dictionary<string, BaseLevelPartObj> tempDicSecLevelObj = new Dictionary<string, BaseLevelPartObj>();
Transform[] tempTransObj = ReadObj.GetComponentsInChildren<Transform>(true);
//采用一次遍历创建完所有的层级菜单项
foreach (Transform item in tempTransObj)
BaseLevelPartObj tempBLPObj = Instantiate(PrefabsPartObj);
PartsLevel tempLevel = (PartsLevel)(GetLevel(item) - 1);
// Debug.Log(item.name + ":" + GetLevel(item) + ":p:"+(item.parent==null?"null":item.parent.name));
switch (tempLevel)
case PartsLevel.None:
break;
case PartsLevel.First:
tempBLPObj.Init(item.name, null, tempLevel);
tempDicFirstLevelObj.Add(item.name, tempBLPObj);
break;
case PartsLevel.Second:
//如果临时一级菜单中有包含当前物体的父物体
if (tempDicFirstLevelObj.ContainsKey(item.parent.name))
BaseLevelPartObj tempParentObj = tempDicFirstLevelObj[item.parent.name];
tempParentObj.HasSubObj();
tempBLPObj.Init(item.name, tempParentObj, tempLevel);
tempDicSecLevelObj.Add(item.name, tempBLPObj);
break;
case PartsLevel.Third:
//如果临时的二级菜单中有包含当前物体的父物体
if (tempDicSecLevelObj.ContainsKey(item.parent.name))
BaseLevelPartObj tempParentObj = tempDicSecLevelObj[item.parent.name];
tempParentObj.HasSubObj();
tempBLPObj.Init(item.name, tempParentObj, tempLevel);
break;
// Update is called once per frame
void Update()
//获取当前的层级
public int GetLevel(Transform transfor)
int tempLevel = 1;
if(transfor.parent!=null)
return tempLevel+ GetLevel(transfor.parent);
return tempLevel;
public enum PartsLevel
None = 0,
First = 1,
Second,
Third,
三、总结
3.1、各个层级展开后对上面层级的影响的处理要特别注意,因为没处理好就会造成展开不正常、展开后折叠时位置不对一级下下层级展开后位置不对的情况;
3.2、下载地址工程文件
以上是关于Unity实用小工具或脚本——可折叠伸缩的多级(至少三级)内容列表(类似于Unity的Hierarchy视图中的折叠效果)的主要内容,如果未能解决你的问题,请参考以下文章
Unity实用小工具或脚本——可折叠伸缩的多级(至少三级)内容列表(类似于Unity的Hierarchy视图中的折叠效果)