Unity UGUI自定义树形菜单(TreeView)

Unity UGUI自定义树形菜单(TreeView)
Unity UGUI自定义树形菜单(TreeView)

Unity UGUI自定义树形菜单

(TreeView)

如果你需要的也是这种效果,那你就来对地方了!

目前,我们这个树形菜单展现出来的功能如下:

1、可以动态配置数据源;

2、点击每个元素的上下文菜单按钮(也就是图中的三角形按钮),可以收缩或展开它的子元素;

3、可以单独判断某一元素的复选框是否被勾选,或者直接获取当前树形菜单中所有被勾选的元素;

4、树形菜单统一控制其下所有子元素按钮的事件分发;

5、可自动调节的滚动视野边缘,根据当前可见的子元素数量进行横向以及纵向的伸缩;

一、首先,我们先制作子元素的模板(Template),也就是图中菜单的单个元素,用它来根

据数据源动态克隆出多个子元素,这里的话,很显然我们的模板是由两个Button加一个Toggle和一个Text组成的,如下:

二、我们的每个子元素都会携带一个TreeViewItem脚本,用于描述自身在整个树形菜单中与其他元素的父子关系,而整个树形菜单的控制由TreeViewControl来实现,首先,TreeViewControl会根据提供的数据源来生成所有的子元素,当然,改变数据源之后进行重

新生成的时候也是这个方法,干的事情很简单,就是用模板不停的创建元素,并给他们建立父子关系:

[csharp] view plain copy

///

/// 生成树形菜单

///

public void GenerateTreeView()

{

//删除可能已经存在的树形菜单元素

if (_treeViewItems != null)

{

for (int i = 0; i < _treeViewItems.Count; i++)

{

Destroy(_treeViewItems[i]);

}

_treeViewItems.Clear();

}

//重新创建树形菜单元素

_treeViewItems = new List();

for (int i = 0; i < Data.Count; i++)

{

GameObject item = Instantiate(Template);

if (Data[i].ParentID == -1)

{

item.GetComponent().SetHierarchy(0);

item.GetComponent().SetParent(null);

}

else

{

TreeViewItem tvi = _treeViewItems[Data[i].ParentID].GetComponent();

item.GetComponent().SetHierarchy(tvi.GetHierarchy() + 1);

item.GetComponent().SetParent(tvi);

tvi.AddChildren(item.GetComponent());

}

https://www.360docs.net/doc/a410024235.html, = "TreeViewItem";

item.transform.FindChild("TreeViewText").GetComponent().text = Data[i].Name;

item.transform.SetParent(TreeItems);

item.transform.localPosition = Vector3.zero;

item.transform.localScale = Vector3.one;

item.transform.localRotation = Quaternion.Euler(Vector3.zero);

item.SetActive(true);

_treeViewItems.Add(item);

}

}

三、树形菜单生成完毕之后此时所有元素虽然都记录了自身与其他元素的父子关系,但他们的位置都是在Vector3.zero的,毕竟我们的菜单元素在创建的时候都是一股脑儿的丢到原点位置的,创建君可不管这么多元素挤在一堆会不会憋死,好吧,之后规整列队的事情就交给刷新君来完成了,刷新君玩的一手好递归,它会遍历所有元素并剔除不可见的元素(也就是点击三角按钮隐藏了),并将它们一个一个的重新排列整齐,子排在父之后,孙排在子之后,以此类推......它会遍历每个元素的子元素列表,发现子元素可见便进入子元素列表,发现孙元素可见便进入孙元素列表:

[csharp] view plain copy

///

/// 刷新树形菜单

///

public void RefreshTreeView()

{

_yIndex = 0;

_hierarchy = 0;

//复制一份菜单

_treeViewItemsClone = new List(_treeViewItems);

//用复制的菜单进行刷新计算

for (int i = 0; i < _treeViewItemsClone.Count; i++)

{

//已经计算过或者不需要计算位置的元素

if (_treeViewItemsClone[i] == null || !_treeViewItemsClone[i].activeSelf)

{

continue;

}

TreeViewItem tvi = _treeViewItemsClone[i].GetComponent();

_treeViewItemsClone[i].GetComponent().localPosition = new Vector3(tvi.GetHierarchy() * HorizontalItemSpace, _yIndex,0);

_yIndex += (-(ItemHeight + VerticalItemSpace));

if (tvi.GetHierarchy() > _hierarchy)

{

_hierarchy = tvi.GetHierarchy();

}

//如果子元素是展开的,继续向下刷新

if (tvi.IsExpanding)

{

RefreshTreeViewChild(tvi);

}

_treeViewItemsClone[i] = null;

}

//重新计算滚动视野的区域

float x = _hierarchy * HorizontalItemSpace + ItemWidth;

float y = Mathf.Abs(_yIndex);

transform.GetComponent().content.sizeDelta = new Vector2(x, y);

//清空复制的菜单

_treeViewItemsClone.Clear();

}

///

/// 刷新元素的所有子元素

///

void RefreshTreeViewChild(TreeViewItem tvi)

{

for (int i = 0; i < tvi.GetChildrenNumber(); i++)

{

tvi.GetChildrenByIndex(i).gameObject.GetComponent().localPosition = new Vector3(tvi.GetChildrenByIndex(i).GetHierarchy() * HorizontalItemSpace, _yIndex, 0);

_yIndex += (-(ItemHeight + VerticalItemSpace));

if (tvi.GetChildrenByIndex(i).GetHierarchy() > _hierarchy)

{

_hierarchy = tvi.GetChildrenByIndex(i).GetHierarchy();

}

//如果子元素是展开的,继续向下刷新

if (tvi.GetChildrenByIndex(i).IsExpanding)

{

RefreshTreeViewChild(tvi.GetChildrenByIndex(i));

}

int index = _treeViewItemsClone.IndexOf(tvi.GetChildrenByIndex(i).gameObject);

if (index >= 0)

{

_treeViewItemsClone[index] = null;

}

}

}

我这里将所有的元素复制了一份用于计算位置,主要就是为了防止在进行一轮刷新时某个元素被访问两次或以上,因为刷新的时候会遍历所有可见元素,如果第一次访问了元素A(元素A的位置被刷新),根据元素A的子元素列表访问到了元素B(元素B的位置被刷新),一直到达子元素的底部后,当不存在更深层次的子元素时,那么返回到元素A之后的元素继续访问,这时在所有元素列表中元素B可能在元素A之后,也就是说元素B已经通过父元素访问过了,不需要做再次访问,他的位置已经是最新的了,而之后根据列表索引很可能再次访问到元素B,如果是这样的话元素B的位置又要被刷新一次,甚至多次,性能影响不说,第二次计算的位置已经不是正确的位置了。

四、菜单已经创建完毕并且经过了一轮刷新,此时它展示出来的就是这样一个所有子元素都展开的形状(我在demo中指定了数据源,关于数据源怎么设置在后面):

我们要在每个元素都携带的脚本TreeViewItem中对自身的那个三角形的上下文按钮监听,当鼠标点击它时它的子元素就会被折叠或者展开:

[csharp] view plain copy

///

/// 点击上下文菜单按钮,元素的子元素改变显示状态

///

void ContextButtonClick()

{

if (IsExpanding)

{

transform.FindChild("ContextButton").GetComponent().localRotation = Quaternion.Euler(0, 0, 90);

IsExpanding = false;

ChangeChildren(this, false);

}

else

{

transform.FindChild("ContextButton").GetComponent().localRotation = Quaternion.Euler(0, 0, 0);

IsExpanding = true;

ChangeChildren(this, true);

}

//刷新树形菜单

Controler.RefreshTreeView();

}

///

/// 改变某一元素所有子元素的显示状态

///

void ChangeChildren(TreeViewItem tvi, bool value)

{

for (int i = 0; i < tvi.GetChildrenNumber(); i++)

{

tvi.GetChildrenByIndex(i).gameObject.SetActive(value);

ChangeChildren(tvi.GetChildrenByIndex(i), value);

}

}

IsExpanding做为每个元素的字段用于设置或读取自身子元素的显示状态,这里根据改变的状态会递归循环此元素的所有子元素及孙元素,让他们可见或隐藏。

五、对所有的子元素进行统一的事件分发,这里主要就有鼠标点击这一个事件:

每个元素都会注册这个事件:(TreeViewItem.cs)

[csharp] view plain copy

void Awake()

{

//上下文按钮点击回调

transform.FindChild("ContextButton").GetComponent