在Unity中要实现如下的树形状结构显示,是比较复杂的,相比于专门做二维的软件,效果也不咋样;但想想毕竟Unity主要是开发三维场景的工具,用来做二维界面确实有点可笑,但是也不是说不能实现,只要Unity有Image,那什么都是可以实现的...【Demo下载地址】
如何实现这种效果呢,主要的难点在哪里?加载数据并保存到对象中不难,利用得到的数据进行UI动态生成才是关键。
程序设计思路:
1、创建一个通用的预制体,加载各级的条目
2、以TreeView对象为父级,加载第一层选项,以每一个条目下的一个组件为父级加载下一级
3、解析xml得到的数据对象转化为条目对象
4、在条目对象自身的脚本下进行初始化
能想到以上一几点基本上也就有了大概思路了;本程序的实现过程使用了pureMvc架构(如果不想用,那解析xml数据的方法直接写入到静态工具类中也未尝不可,只是程序的耦合度增加),UI界面相对比较独立
那么具体实现需要进行以下几点的深入:
1、制作数据模型
2、解析xml数据到数据模型
3、正确使用pureMVC架构
4、制作UI预制体
5、将数据对象转换为UI对象
6、动态显示功能
7、事件注册
这些问题 是在制作程序的过程中遇到的,也算是一点小经验吧,不一定都能适用,实现TreeView的效果也可能只有这一种。
下面是具体实现:
一、数据模型:
树形图数据的一个特点就是父节点有一堆子节点,和xml数据差不多(解析起来也比较容易),定义了一个XMLDataProxy类,有一个父结点和子结点列表
public class XMLDataProxy : Proxy {
public XMLDataProxy(string name):base(name){
}
public XMLDataProxy ParentNode { get; set; }
public List
}
二、解析XML数据到XMLDataProxy
由于程序使用了PureMVC架构,所以这里直接用Commond类来实现以上数据的解析和注册
public class XMLLoadCommond : SimpleCommand
{
public override void Execute(INotification notification)
{
string xmlPath = Application.streamingAssetsPath + "/Projects.xml";
XMLDataProxy m_XmlDataProxy = XMLParse(xmlPath); //解析数据的核心
AppFacade.Instance.RegisterProxy(m_XmlDataProxy); //将解析得到的数据注册了Model
AppFacade.Instance.SendNotification(NotiConst.ThreeView); //通知View层进行解析
}
///
/// 从XML源加载数据
///
///
///
public static XMLDataProxy XMLParse(string fileName)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(fileName);
XmlNode rootNode = xmlDoc.SelectSingleNode("Projects");
XMLDataProxy rootProxyNode = new XMLDataProxy("Project");
XMLDataProxyAppendChild(rootNode, rootProxyNode); //解析第一层数据
return rootProxyNode;
}
///
/// 利用XMLNode创建xmlDataProxy
///
///
///
public static void XMLDataProxyAppendChild(XmlNode xNode, XMLDataProxy parent) //递归遍历所有子结点数据
{
if (!xNode.HasChildNodes)
{
return;
}
else
{
foreach (XmlElement item in xNode)
{
XMLDataProxy cNode = new XMLDataProxy(item.GetAttribute("name"));
cNode.ParentNode = parent;
if (parent.ChildNodes == null)
{
parent.ChildNodes = new List
}
parent.ChildNodes.Add(cNode);
XMLDataProxyAppendChild(item, cNode);
}
}
}
}
三、关于PureMvc的使用
pureMVC中的mvc的对象要经过注册也能使用,实现INotifier接口的对象一般有三种发送信息的方式,一是只发送通知的内容,二是发送通知的内容和数据包(object)三是还要发送类型。其中最常用的是第二种,对指定的观察者发送一个数据包。但由于加载 的数据常常不是死的,而是动态加载出来的而且常常不是一个简单的int,float ,string 和bool等类型。更为高级的方式是将proxy 数据注册到model中,在Media需要的时候查找出来就可以了。而加载数据这个过程,交给commond最适合不过了,相对于静态工具类也更为合理。
1.commond类的注册与执行
void Start () {
AppFacade.Instance.RegisterCommand(NotiConst.LoadProject, typeof(XMLLoadCommond));
}
// Update is called once per frame
void OnGUI()
{
if (GUILayout.Button("加载xml文档"))
{
AppFacade.Instance.SendNotification(NotiConst.LoadProject);
}
}
2、meida类的注册
public class TreeView : Mediator
{
private XMLDataProxy m_XmlDataProxy;
private GameObject itemPfb;//一级节点
public override IList
{
return new List
}
public override void HandleNotification(INotification notification)
{
//将信息加载到View
m_XmlDataProxy = AppFacade.Instance.RetrieveProxy("Project") as XMLDataProxy;
LoadAllNodes();
}
void Awake()
{
itemPfb = transform.Find("Item").gameObject;
AppFacade.Instance.RegisterMediator(this);
}
}
四、制作预制体
1、根目录上添加两个组件,一个是动态调整,一个是垂直列表
2、通用预制体对象
将Item作为根目录的子物体,这样就可以实现列表效果了
这个过程最最关键的是锚点问题,可以从上图看到,TreeView对象的锚点在左上角的同时,其中心点也必须是左上角,因为动态加载Item时,希望是最高点不发出动,整个向下扩张。
在第一级制作成功后,想第二级当然也要实现如第一级一样的扩展性,于是在item下创建了一个Panel,也增加了如TreeView的两个组件。但为什么要放置在Contant下,见下图:
如果想偷懒不在代码中来控制Panel的坐标,这个方法实现是不错,在Panel下增加一个item后的效果变成:
很显然后Panel的对齐方式是右上角对齐...也就是说只需要一个item对象,就可以创建无限多个层级了。其实有人已经发现了这个过程中每创建出来一个末端就会多出一个Content和一个Panel,这位下来影响程序的性能,遇到这个问题,其实将panel和item拆开加载也是可以的。,这个程序还是很有优化的空间的,在说吧...
五,将数据对象转换为UI对象
这个过程是在TreeView得到了数据后进行的,由于创建对象过程中需要进行多个调整,最好的办法还是创建一个TreeItem脚本,来操作创建出来 的对象:
public class TreeItem : MonoBehaviour
{
public string NodeName {
get
{
return GetComponentInChildren
}
set
{
GetComponentInChildren
}
}//中文名
public Transform Parent {
set { transform.SetParent(value); }
get { return transform.parent; }
}
public Transform ClildPanel {
get { return transform.Find("Contant/Panel"); }
}
public ToggleGroup ToggleGroup{
set { GetComponent
}//设置group
private bool isLastOne;//最后一层,取消自动选中
private bool Selected
{
get { return ClildPanel.gameObject.activeSelf; }
set { ClildPanel.gameObject.SetActive(value); }
}//是否选中(同级之中最多只有一个可以选中)
private Toggle m_toggle;
void Awake () {
ClildPanel.gameObject.SetActive(false);
m_toggle = GetComponent
m_toggle.onValueChanged.AddListener((x)=> { Selected = x; });
}
void Start()
{
transform.localScale = Vector3.one;
}
}
将对象上的一些组件性质与属性进行绑定,这样,只要对这些属性进行赋值和取值就可以了
在TreeView获得数据后进行创建的过程写在自身脚本中:
public class TreeView : Mediator
{
private XMLDataProxy m_XmlDataProxy;
private GameObject itemPfb;//一级节点
public override IList
{
return new List
}
public override void HandleNotification(INotification notification)
{
//将信息加载到View
m_XmlDataProxy = AppFacade.Instance.RetrieveProxy("Project") as XMLDataProxy;
LoadAllNodes();
}
void Awake()
{
itemPfb = transform.Find("Item").gameObject;
AppFacade.Instance.RegisterMediator(this);
}
private void LoadAllNodes()
{
ToggleGroup tgg = gameObject.AddComponent
foreach (var item in m_XmlDataProxy.ChildNodes)
{
CreateQuaders(item, transform, itemPfb,tgg);
}
itemPfb.SetActive(false);
AppFacade.Instance.RemoveCommand(NotiConst.LoadProject);
}
public void CreateQuaders(XMLDataProxy data, Transform parent, GameObject btnpfb,ToggleGroup tgg) //递归全部创建
{
TreeItem treeItem = Instantiate(btnpfb).GetComponent
treeItem.NodeName = data.ProxyName;
treeItem.Parent = parent;
treeItem.ToggleGroup = tgg;
if (data.ChildNodes != null && data.ChildNodes.Count > 0)
{
ToggleGroup Newtgg = treeItem.gameObject.AddComponent
foreach (var item in data.ChildNodes)
{
CreateQuaders(item, treeItem.ClildPanel, btnpfb, Newtgg);
}
}
else//给最后一级toggle添加事件
{
treeItem.ToggleSelected(OnLastItemSelected);
}
}
private void OnLastItemSelected(string itemName)
{
Debug.LogWarning("you need notify"+itemName);
}
}
六、动态显示功能
动态显示鼠标移动到的对象上,这些条目本身是考虑用Button的,但想到这个问题的时候,实现过程比较困难,还需要考虑哪些条目刚刚打开了,哪些条目需要进行关闭。最后想到Toggle有一个属性是可以实现多个Toggle同时只有一个是isOn。这样一想和treeView的效果简直就是一模一样嘛。于是在创建对象的过程上中只需要在父级上添加 一 个ToggleGroup,并将Toggle的这个属性赋上这个ToggleGroup。这样就实现了点击打开对应的项目。
说好的鼠标移动到对象上可以动态显示呢,不急,点击都实现了还差一个OnMouseEnter类似的功能么,当然,在UGUI上鼠标移入的事件是继承了IPointerEnterHandler,完善条目脚本TreeItem如下,其中点击事件的注册也写入了,值得注意的最后一级常常需要触发不同的事件(不单单是展开),于是在创建对象的时候判断并将OnLastItemSelected这个方法注册到上上点中事件。
public class TreeItem : MonoBehaviour,IPointerEnterHandler {
public string NodeName {
get
{
return GetComponentInChildren
}
set
{
GetComponentInChildren
}
}//中文名
public Transform Parent {
set { transform.SetParent(value); }
get { return transform.parent; }
}
public Transform ClildPanel {
get { return transform.Find("Contant/Panel"); }
}
public ToggleGroup ToggleGroup{
set { GetComponent
}//设置group
private bool isLastOne;//最后一层,取消自动选中
private bool Selected
{
get { return ClildPanel.gameObject.activeSelf; }
set { ClildPanel.gameObject.SetActive(value); }
}//是否选中(同级之中最多只有一个可以选中)
private Toggle m_toggle;
void Awake () {
ClildPanel.gameObject.SetActive(false);
m_toggle = GetComponent
m_toggle.onValueChanged.AddListener((x)=> { Selected = x; });
}
void Start()
{
transform.localScale = Vector3.one;
}
public void OnPointerEnter(PointerEventData eventData)
{
m_toggle.isOn = !isLastOne;
}
///
/// 点击回调
///
///
public void ToggleSelected(UnityAction
{
isLastOne = true;
m_toggle.onValueChanged.RemoveAllListeners();
m_toggle.onValueChanged.AddListener((x) => { if (x) action(NodeName); });
}
}