原文地址:
http://www.billdwhite.com/wordpress/?p=296
当我了解到Flex4那些对我诸多裨益的新特性后, 我便决定转而使用它。刚开始的时候,我试图利用在Flex前作中的认识和既有经验来快速进入状态。但很快我便发现有时即使面对一些显而易见的问题我也不得不求助于API文档或者运行一些示例程序来弄清这种问题的来龙去脉。根据以往经验,Flex3 的Halo在处理显示列表的时候隐藏了大量的实现细节和不良设计。然而一旦你开始使用新的Spark架构后,你就得以近距离的面对这些实现细节—Halo究竟在私底下干了什么,而且你会体会到为什么说Spark对于显示列表的处理更为“直白”。
“elements”是一个关键性的问题。elements是何物?它同child是否是一回事?刚开始的时候我曾武断的认为elements不过是children的另一种说法。通过反复梳理组件中所有的elements和children,我发觉在新的容器类(也包括一些经过改良的传统容器)某些似乎是理所当然应该具备的方法消失了。如果没有getElements(),我该如何获取elements的数目呢?我能否把getChildren() 的返回结果作为IVisualElement来对待。这令我十分纠结。
困扰的我于是开始认真阅读学习API文档,Flex的源码以及相关的博客文章。我也曾尝试解读一些博主发布的关于Flex4新特性的幻灯片。然而事实证明脱离讲解而孤立的看幻灯片作用相当有限。
最后,我拼凑了一些言简意赅的示例。这些示例将带领我了解有关elements的全新知识,告诉我那些在新的Spark容器背后发生的故事。
言归正传,首先从问题开始。问题一,“应该如何获得Spark 容器的全部elements?”我曾想当然的认为是通过一个类似Flex3中的getChildren() 的方法。然而实际上我们需要通过两个Property来达到这个目的:numElements & numChildren 。可以通过对numElements计数的循环语句配合getElementAt() 来实现遍历容器elements或特定访问。这种方式还比较直观。问题二,“element和child的区别何在?”,让我们来看看两者的差异。
语义上,element简单的说就是实现了IVisualElement接口的任意型别。child是指扩展了DisplayObject类的任意型别。判断某个组件是element还是child亦或两者都是的关键在于以下几点。UIComponent(所有Flex组件的基类:译者注)是由DisplayObject扩展而来,故所有UIComponent都是DisplayObject,也就是说UIComponent都是children。UIComponent同时也实现了IVisualElement接口,因而所有的UIComponent也可以被作为elements看待。但这并不是说所有的DisplayObjects(文中所言的DisplayObject一般指扩展于DisplayObject的子类,译者注)都是elements。容器中的DisplayObject对象是该无疑是容器的child。而只有当此DisplayObject对象同时也实现了IVisualElement接口时它才是容器的element。那么对容器而言,DisplayObject什么情况下是child,什么情况下又是element?通过示例来认识这个问题。
在首个示例中,我们使用了传统的Halo容器(这里我们使用的Panel)。Panel扩展与DisplayObject类,所以它可以使用addChild() 方法。进一步而言,Panel也是Container类的子类(mx.core.Container实现了IVisualElementContainer接口),它具有addElement() 方法。Container类的IVisualElementContainer接口实现只是基于显示列表API的门面,所以理论上它和同样实现了IVisualElementContainer接口的新式Spark容器具有相同的方法集合。
于是看起来我们可以任意添加children或element到容器中了。事实却不是这样。并非任意型别的element都能被添加(此处element泛指实现了IVisualElement接口的类)容器中。视觉元素(VisualElements)和图形元素(GraphicElements)有一些区别视觉元素(VisualElements)实现了IVisualElement接口,而图形元素(GraphicElements)实现的是IVisualElement接口的子接口IGraphicElement。IGraphicElement接口扩展的新特性为容器获取信息提供了额外渠道。某些elements(图形元素是其中之一)无法直接添加至Halo的Panel编译器会告知“这样的对象需事先包装进一个Group容器中”(实际上错误提示应该是在运行时出现,不关编译器什么事:译者注)。原因马上揭晓。
接下来的示例中,Panel中有若干个UIComponent,其中包括另一个Halo Panel,一个Spark Panel,几个Halo Button和几个Spark Button,以及一个包含有子组件的SkinnableContainer(注意: 包含于SkinnableContainer的组件是只属于SkinnableContainer的children,不是上级容器Panel的children)。所有组件都继承于DisplayObject,所以它们都是“children”。点击“show children”后可以清楚的了解这一点。进一步而言,所有的组件也都是“element”,因为UIComponent实现了IVisualElement接口。
看下一个示例。这次我们探讨的容器上Spark Group。与前Halo Panel类似,Group继承于DisplayObjectContainer,它具有addChild() 方法,它同时也实现了IVisualElement接口,所以我们可以用addElement() 方法来IVisualElement对象(elements)。而且Group也接受图形元素(GraphicElements),比如spark.primitives.Rect。要知道Rect是无法直接添加到Halo Panel中的。Group是怎么做到这一点的?原因就在于Group知道如何使用一种优化的方式来呈现图形元素(GraphicElements)。什么意思?往下读。
相对于典型的视觉元素(VisualElements),图形元素(GraphicElements)与容器的关系更为紧密。其关键在于IGraphicElement接口。上面曾经提到,这个扩展于IVisualElement的接口(此即图形元素(GraphicElements)可以通过Group的addElement() 方法来添加至其上的原因所在)。然而由于图形元素(GraphicElements)不是DisplayObject,所以他们在被“投映”到某个作为他父对象的DisplayObject前是无法被显示出来的。基于这个原因,当添加一个“Rectangle”到Group时,需要有DisplayObject来绘制这个Rectangle。更有效率一点的做法是Group尽可能的复用同一个DisplayObject来绘制多个图形元素(GraphicElements)。容器可以使用任何实现了ISharedDisplayObject接口的DisplayObject来绘制图形元素(GraphicElements)。第一个示例中的Halo Panel无法使用这种方式来绘制图形元素(GraphicElements),编译器会报错:“必须将其包装至一个合适的容器中”。而Group支持这种优化方式,所以能添加图形元素(GraphicElements)。
另外需要注意的一点是,有些图形元素(GraphicElements)的绘制由Group提供DisplayObject来完成,也有的是自行创建专属的DisplayObject来完成绘制。IGraphicElement接口甚至允许把对象自己创建的DisplayObject交由容器管理(换而言之就是以child形态添加的DisplayObject会以IGraphicElement的面貌来绘制自己)。
这意味着什么?这意味着在接下来的示例中,children的数目和elements的数目是不一样的。这个示例使用了与第一个示例相同的组件集合外,还增加了4个矩形图形元素(GraphicElements)。所有子对象皆为IVisualElement,但不是都可以称为children。几个矩形是图形元素(GraphicElements),它们并不继承于DisplayObject。Group不在乎这点,它知道添加DisplayObject来绘制这些图形元素(GraphicElements)。由于几个矩形的尺寸和角度有所不同,所以Group会创建2个新的DisplayObject来绘制这4个矩形。很酷吧!
现在来看示例三。我们用一个SkinnableContainer替换先前的Group。SkinnableContainer有和先前相同的子组件集,它还能利用Skin来增强视觉效果。Skin是SkinnableContainer唯一的child。SkinnableContainer的默认Skin类由一个矩形和一个被称为ContentGroup的Group组成。该Group的作用在于规划出容器内组件的添加位置。
这个示例证明了这样的事实,即使SkinnableContainer拥有10个elements,但它只有唯一的child:它自己的Skin。而且这个Skin也只有唯一的child:名为ContentGroup的Group组件。你也许会感到奇怪:为什么Skin的children不是2个:其一是ContentGroup,另一个是用于绘制作为边框的Rectangle的DisplayObject?这是因为Skin类继承自Group类,而Group只在它确实需要绘制其包容的图形元素(GraphicElements)时才会添加DisplayObject,目前的情况下不需要。Skin类具备直接在其上绘制Rect图形元素(GraphicElements)的能力,这归功于Skin类的上级类Group实现了ISharedDisplayObject接口。这意味着它在需要时能作为共享的DisplayObject来绘制图形元素(GraphicElements)。Skin负责管理用于呈现图形元素(GraphicElements)的DisplayObject,在当前示例中,Skin自己就是用于绘制的DisplayObject!如果你的自定义Skin中有其它的Rectangle,并将该Skin赋予SkinnableContainer,这种情况下Skin会判断是否需要更多的DisplayObject来绘制额外的Rectangle。这时你可能会发现在Skin的children列表中有更多的child。
值得注意的是,示例中SkinnableContainer,它的Skin以及Skin的ContentGroup这三者的element列表的数目是相同的。通过SkinnableContainer的源码可以知道,numElement的值实际上来源于与之对应的CurrentContentGroup的numElement。所以基本上对SkinnableContainer的elements的检索是被重定向到它的ContentGroup上的。SkinnableContainer的Skin也有类似行为。它继承于Group,Group的numElement的值取自其内部的mxmlContent属性。该属性是一个保存了Group可视内容children的数组。这两个属性与Panel的RawChildren属性十分相似,它用于返回Panel上的所有children而不是getChildren()方法返回的仅仅你添加到Panel上的那些。
通过以上阅读,也许起不到拨云见日的效果。但可以让你明白厘清以下七个类/接口的继承结构和相互关系是十分有必要的:
1. DisplayObject
2. UIComponent
3. Container
4. IVisualElement
5. IGraphicElement
6. IVisualElementContainer
7. ISharedDisplayObject
一旦你掌握它们之间的关系,你就能明白elements 和children的不同。可以肯定的是我在某些问题的认识和阐述上存在很多谬误之处。如果你发现了这样的问题望不吝赐教,在评论处写下您的正确观点吧。
更新:访问下面的链接可以获得关于本文探讨及其相关的主题的更多内容。
Gumbo DOM Tree API(关于本话题的详实细节)
关于了解组件的owner和parent之间差异的
极好的示例