自己动手写一个给予UGUI的TreeView

自己动手写一个基于UGUI的TreeView

前言

最近搞的BIM工程中有个PDF图纸浏览的需求,然而需要一个TreeView来列出所有的PDF文档,网上搜了下,没大有很相中的插件,于是自己思考了一下,决定自己写一个。

效果图

自己动手写一个给予UGUI的TreeView
自己动手写一个给予UGUI的TreeView
哎,本来想截一个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 )

所谓“兄弟顺序”,就是在同一个父物体下面,它和别的兄弟的排行,如果还不理解,参见下图:自己动手写一个给予UGUI的TreeView
有了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!");
    }
}

猛击此处下载工程源码

上一篇:Tkinter 之TreeView表格与树状标签


下一篇:WPF中的TreeView