自己动手写一个基于UGUI的TreeView
前言
最近搞的BIM工程中有个PDF图纸浏览的需求,然而需要一个TreeView来列出所有的PDF文档,网上搜了下,没大有很相中的插件,于是自己思考了一下,决定自己写一个。
效果图
哎,本来想截一个GIF动图的,但是截出来的图很大,10M+,CSDN上传图片限制是5M,没办法截动图了。
原理介绍
事实上,对于一个没有层级的ListView来讲,估计大家都会弄,就是把Prefab实例化一下,放进View的Content中完事了,Tree的难点在于可折叠、可展开。其实捋明白了也没啥难事。
本来的想法是,在unity中构造一个层级相同的树结构,但是发现这样搞会带来很多问题,尤其是在计算折叠上,需要递归的计算Item的高度,非常复杂,而且效率不高,后来灵光一现,为何非要把unity的物体层级搞得跟树结构一样呢?所有的节点都是兄弟的情况下,反而很容易就能实现!!
首先,Content中要有一个VerticalLayoutGroup组件,这个组件比较关键,他的作用就是让你后来实例化进来的Prefab能自动垂直方向排列,然后再有一个ContentSizeFilter组件,把垂直方向设置为PreferredSize,这样,就能根据实际自动计算Content的Size,从而自动改变滚动条的大小以及处理滚动事件了。
还有一个关键点,就是被Disable的物体,VerticalLayoutGroup以及ContentSizeFilter在计算布局和尺寸时,是会自动忽略的!!
那么,基于上述两点,我们就可以实现Tree了。当一个Item被折叠时,只要把它的孩子Disable掉,那么下面的节点就会被VerticalLayoutGroup和ContentSizeFilter自动“提上来”。这就是基本思路。
有了这个思路,那么问题的关键就在于,保证所有Item的顺序是证确的,就是说,**对于整个树来讲,要保证每一个孩子节点都在其父节点的后面!!**而不会跑到别的节点后面。
做到上面这一点,用到两个方法:
///获取一个Unity物体的兄弟顺序索引
transform.GetSiblingIndex()
///设置一个Unity物体的兄弟顺序索引
transform.SetSiblingIndex( int index )
所谓“兄弟顺序”,就是在同一个父物体下面,它和别的兄弟的排行,如果还不理解,参见下图:
有了API,剩下的就是敲代码了。
/// 计算并获取一个节点的包括它所有孩子们以及孩子的孩子们在
/// 内的所有物体的,最后应该的,兄弟次序
public int LastSiblingIndex
{
get
{
if (IsHasChildren)
return Children[Children.Count - 1].LastSiblingIndex;
else
return transform.GetSiblingIndex() + 1;
}
}
/// 设置一个节点的父亲
public override TreeItemBase Parent
{
get { return m_Parent; }
set
{
if ( m_Parent != value)
{
if (m_Parent != null)
m_Parent.Children.Remove(this);
m_Parent = value;
/// 获取这个节点应该所处的位置
int index = m_Parent.LastSiblingIndex;
/// 把这个节点设置到它应该在的位置上
transform.SetSiblingIndex(index);
m_Parent.Children.Add(this);
RecalcIndentation();
if (m_Parent.gameObject.activeSelf && m_Parent.IsExpanded)
gameObject.SetActive(true);
else
gameObject.SetActive(false);
}
}
}
哦,还有一个点,就是每个节点要有适当的缩进。这个比较简单,每个节点的层级就是它的父亲的层级加1,我们很容易计算:
/// 考虑到效率,我们这样做:
/// 如果一个节点的层级没有变化,那么我们只在它第一次需要计算
/// 层级的时候才去计算它,然后再次访问的时候,就直接返回已经
/// 计算好了的层级数。
private int m_IndentationLevel = -1;
public override int IndentationLevel
{
get
{
if (m_IndentationLevel < 0)
{
if ( Parent.IsRoot)
m_IndentationLevel = 0;
else
m_IndentationLevel = Parent.IndentationLevel + 1;
}
return m_IndentationLevel;
}
}
然后就是缩进了。
/// 重新计算一个节点的缩进,包括计算它的孩子们,
/// 以及孩子们的孩子们,然后重新布局
internal override void RecalcIndentation()
{
if (Parent.IsRoot)
m_IndentationLevel = 0;
else
m_IndentationLevel = Parent.IndentationLevel + 1;
Vector2 offset = ContentPanel.offsetMin;
offset.x = TheTree.IndentationWidth * m_IndentationLevel;
ContentPanel.offsetMin = offset;
foreach (var c in Children)
c.RecalcIndentation();
}
当然,真正的缩进要稍微再复杂一点点,因为要考虑节点是否有图标,没有的话,文字要往左一点,有的话,就把图标的位置留出来。
核心的原理就是这些了。。。当然,其他代码还是很多的,而且我们还可以提供其他的易用的API接口,比如:
//获取或者设置一个节点是否被展开
public bool IsExpaned {get;set;}
/// 递归的展开或者收缩一个节点
public void ExpandRecursive(bool Expand );
/// 展开或收缩一个节点,但不触发OnExpaned事件
public void ExpandWithOutEventTrigger(bool Expand)
/// 设置一个节点的父亲(可以动态改变树的结构)
public void Parent{get;set;}
/// 设置、获取一个节点的附加自定义数据
public void SetBindData( object data );
public T GetBindData<T>();
// ..........................................
/// 添加一个节点
public void AppendItem( string title, Sprite icon, TreeItem parent=null);
// ................................
// ..........这里省略n行代码(10000>n>10)
最终,这个TreeView经过封装后,使用方法如下:
public class TreeTest : MonoBehaviour
{
[SerializeField]
private TreeView m_Tree = null;
// 存储到树中的自定义数据
private class TreeNode
{
public bool IsDirection = false;
public string fullName = null;
}
protected void Awake()
{
// 监听树的事件,树事件很多,这里只监听两个
m_Tree.OnItemExpanded += new TreeView.OnItemExpandedHandler(OnDirectionExpand);
m_Tree.OnItemSelected += new TreeView.OnItemSelectedHandler(OnSelectDoc);
}
/// 当一个节点被选择时,触发
private void OnSelectDoc(TreeItem item)
{
TreeNode node = item.GetBindedData<TreeNode>();
if (node != null)
{
// 如果节点是个目录,当被选择时,就反转树的展开状态
if (node.IsDirection)
item.ExpandInvert();
}
}
// 当一个节点被展开时触发
private void OnDirectionExpand(TreeItem item, bool bExpand)
{
// 更换一下文件夹的图标
item.IconSprite = bExpand ? m_Tree.IconsArray[1] : m_Tree.IconsArray[0];
}
private void Start()
{
string path = "D:\\Temp\\Docs";
ListDirection(null, path);
m_Tree.ExpandAll();
}
// 根据文件目录结构构造树
private void ListDirection(TreeItem node, string path)
{
if (Directory.Exists(path))
{
DirectoryInfo info = new DirectoryInfo(path);
foreach (var f in info.GetFileSystemInfos())
{
if (f is DirectoryInfo d)
{
TreeItem item = m_Tree.AppendItem(d.Name, 0, node);
item.BindData(new TreeNode { IsDirection = true, fullName = d.FullName });
ListDirection(item, d.FullName);
}
else
{
// 跳过不是PDF的其他文件
if (string.Compare(f.Extension, ".pdf", true) == 0)
{
TreeItem item = m_Tree.AppendItem(Path.GetFileNameWithoutExtension(f.Name), 2, node);
item.BindData(new TreeNode { IsDirection = false, fullName = f.FullName });
}
}
}
}
else
Debug.Log($"[{path}] Not Exists!");
}
}
猛击此处下载工程源码