当我了解到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的不同。可以肯定的是我在某些问题的认识和阐述上存在很多谬误之处。如果你发现了这样的问题望不吝赐教,在评论处写下您的正确观点吧。