英文原文: Flex 4 Gumbo DOM Tree API - Functional and Design Specification
翻译的原创链接: http://www.smithfox.com/?e=36 转载请注明, 文中如果有什么错误的地方或是讲的不清楚的地方,欢迎大家留言.
这是一篇难得的Flex功能和架构技术SPEC, 耐心看完绝对有收获.
为了振作你看这个文章的兴趣, 假设你应聘Flex工作被问到了下面的几个问题:
1. Flex中owner和parent有什么区别?
2. addChild和addElement两套函数有什么不同,(不是指怎么使用不同, 而是指框架内部的设计有什么不同)?
3. <s:Rect>是GraphicElement吗, 他们为什么可以放在<s:Group>内?
4. SkinnableComponent, SkinnableContainer, Group, DataGroup以及SkinnableDataContainer有什么区别?
5. 最关键的是: 你知道smithfox吗?(哈哈)
目的
在Flex 4中有许多DOM(Document Object Model)树。他们到底是怎么组织和呈现的?
定义
图形元素(graphic element) - 就象是矩形, 路径, 或是图片. 这些元素不是DisplayObject的子类; 但是它们还是需要一个DisplayObject来渲染到屏幕. (smithfox注: "多个图形元素可以只用一个DisplayObject来渲染")
视觉元素(visual element) - (英文有时简称为 - "element"). 可以是一个halo组件, 或是一个gumbo组件, 或是一个图形元素. 视觉元素实现了接口 IVisualElement.
数据项 (英文有时简称为 - "item") - 本质上Flex中的任何事物都可以被看着数据项. 通常是指非可视化项,比如 String, Number, XMLNode, 等等. 一个视觉元素也能作为数据项 -- 这要看他是怎么被看待的.
组件树 - 组件树表现了MXML文档结构. 举个简单例子, 一个 Panel 包含了一个 Label. 这个例子中, Panel 和 Label 都在组件树中, 但是 Panel的皮肤却不是.
布局树 - 布局树呈现了运行时的布局. 在这个树中, 父亲负责呈现和布局对象, 孩子则是被布局的视觉元素. 举个简单例子, 一个 Panel 包含了一个 Label. 这个例子中, Panel 和 Label 都在布局树中, 同样Panel的皮肤和皮肤中的contentGroup也是.
显示树 - Flash 底层 DisplayObject 树.
本文中的全部图的图例如下:
背景:
当你用MXML创建应用程序时, 幕后发生了许多的事情,会将MXML转换成Flash显示对象. 后台有三个主要因素: 皮肤,项渲染和显示对象sharing. 前两个对开发人员是非常重要的概念; 最后一个只需要框架开发人员关注, 但仍然比较重要.
皮肤:
当你初始化一个 Button, 其实创建了不止一个对象. 例如:
<s:Button />
在布局树中的结果是:
(注: TextBox 已经更名为 Label)
一个皮肤文件被实例化了,并且加入到Button的显示列表中.Button的皮肤文件如下:
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" minWidth="23" minHeight="23"> <fx:Metadata> [HostComponent("mx.components.Button")] </fx:Metadata> <s:states> <s:State name="up" /> <s:State name="over" /> <s:State name="down" /> <s:State name="disabled" /> </s:states> <!-- background --> <s:Rect left="0" right="0" top="0" bottom="0" width="70" height="23" radiusX="2" radiusY="2"> <s:stroke> <s:SolidColorStroke color="0x5380D0" color.disabled="0xA9C0E8" /> </s:stroke> <s:fill> <s:SolidColor color="0xFFFFFF" color.over="0xEBF4FF" color.down="0xDEEBFF" /> </s:fill> </s:Rect> <!-- label --> <s:Label id="labelDisplay" /> </s:Skin>尽管Button看上去是一个叶子结点, 但因为皮肤的存在, 实际上他包含了孩子. 为访问这些元素,所有SkinnableComponent对象都定义了skin属性. 这样就可以通过Button.Skin实例来访问Rectangle 和Label. 如要访问Label, 你可以写成:myButton.skin.getElementAt(2)或是 myButton.skin.labelDisplay.由于labelDisplay是 Button 的 skin part, 所以你可可以直接写成 myButton.labelDisplay.
同样的原则也一样适用在SkinnableContainer. SkinnableContainer是容器所以天然就有孩子, 但同时他们也是SkinnableComponent,所以也有一个皮肤以及来自皮肤的孩子.
(smithfox注: SkinnableContainer的确是继承自SkinableComponent, 见图)
还是以Panel为例:
<s:Panel> <s:Button /> <s:Label /> <s:CheckBox /> </s:Panel>panel有三个孩子:一个button,一个label,和一个checkbox.用定义在SkinnableContainer上的content APIs可以访问他们. 这些content APIs很像flash DisplayObjectContainer 的 APIs, 包括addElement(), addElementAt(), getElementAt(), getElementIndex(), 等等.... 所有方法的完整列表在稍后文档中列出.
因为 panel有3个孩子, 它的组件树象这样:
(注: TextBox 已经更名为 Label)
但是, 这只是组件树. 因为皮肤的原因, Panel真正布局树是这样的:
(注: TextBox 已经更名为 Label)
在上面这张图上有许多箭头. 需要注意的有:
- Panel的组件孩子有: button, label, 和checkbox.
- button, label, 和checkbox的组件父亲(owner 属性) 是 Panel.
- button, label, and checkbox的布局父亲 (parent 属性) 是 Panel皮肤的contentGroup.
这意味着即使看上去Panel的孩子应该是一个button, 一个label, 和一个checkbox; 但实际上真正的孩子是一个panel皮肤实例. button, label, 和 checkbox 向下变成了皮肤中contentGroup的孩子. 有几种方法可以访问panel中的Button: myPanel.getElementAt(0) or myPanel.contentGroup.getElementAt(0) or myPanel.skin.contentGroup.getElementAt(0).
所有 SkinnableComponent 都有 skin 属性. 在 SkinnableContainer中组件的孩子实际上下推成为skin的 contentGroup的孩子. 组件树 指向编译自MXML的语义树.Panel 例子中, 只包括Panel 和他的孩子: 一个 button, 一个 label, 和一个checkbox. 由于皮肤, 布局树 是布局系统所实际看到的树.Panel 例子中,包括 这个panel, panel的皮肤, 以及这个皮肤的所有孩子(皮肤中的contentGroup的孩子).
布局树无需和所见的Flash显示列表有什么相关性. 这是因为 GraphicElement 不是天然的显示对象. 因为考虑效率的原因, 他们最小化了显示对象数目(smithfox注: 多个GraphicElement可以在一个DisplayObject上渲染, 这样DisplayObject的总数就可以大大减少).
(smithfox注: GraphicElement是spark的类, 确实是少有继承层次非常少的对象, 如图:)
IVisualElementContainer 定义了content APIs. 在Spark中, Skin, Group, 和 SkinnableContainer 实现了这个接口,持有着可视化元素. 为保持一致性, MX的 Container 也实现了这个接口, 不过只是对addChild(), numChildren, 等函数的封装....
package mx.core { public interface IVisualElementContainer { //---------------------------------- // Visual Element iteration //---------------------------------- /** * The number of elements in this group. * * @return The number of visual elements in this group */ public function get numElements():int; /** * Returns the visual element that exists at the specified index. * * @param index The index of the element to retrieve. * * @return The element at the specified index. * * @throws RangeError If the index position does not exist in the child list. */ public function getElementAt(index:int):IVisualElement //---------------------------------- // Visual Element addition //---------------------------------- /** * Adds a visual element to this visual container. The element is * added after all other elements and on top of all other elements. * (To add a visual element to a specific index position, use * the <code>addElementAt()</code> method.) * * <p>If you add a visual element object that already has a different * container as a parent, the element is removed from the child * list of the other container.</p> * * @param element The element to add as a child of this visual container. * * @return The element that was added to the visual container. * * @event elementAdded ElementExistenceChangedEvent Dispatched when * the element is added to the child list. * * @throws ArgumentError If the element is the same as the visual container. */ public function addElement(element:IVisualElement):IVisualElement; /** * Adds a visual element to this visual container. The element is * added at the index position specified. An index of 0 represents * the first element and the back (bottom) of the display list, unless * <code>layer</code> is specified. * * <p>If you add a visual element object that already has a different * container as a parent, the element is removed from the child * list of the other container.</p> * * @param element The element to add as a child of this visual container. * * @param index The index position to which the element is added. If * you specify a currently occupied index position, the child object * that exists at that position and all higher positions are moved * up one position in the child list. * * @return The element that was added to the visual container. * * @event elementAdded ElementExistenceChangedEvent Dispatched when * the element is added to the child list. * * @throws ArgumentError If the element is the same as the visual container. * * @throws RangeError If the index position does not exist in the child list. */ public function addElementAt(element:IVisualElement, index:int):IVisualElement; //---------------------------------- // Visual Element removal //---------------------------------- /** * Removes the specified visual element from the child list of * this visual container. The index positions of any elements * above the element in this visual container are decreased by 1. * * @param element The element to be removed from the visual container. * * @return The element removed from the visual container. * * @throws ArgumentError If the element parameter is not a child of * this visual container. */ public function removeElement(element:IVisualElement):IVisualElement; /** * Removes a visual element from the specified index position * in the visual container. * * @param index The index of the element to remove. * * @return The element removed from the visual container. * * @throws RangeError If the index does not exist in the child list. */ public function removeElementAt(index:int):IVisualElement; //---------------------------------- // Visual Element index //---------------------------------- /** * Returns the index position of a visual element. * * @param element The element to identify. * * @return The index position of the element to identify. * * @throws ArgumentError If the element is not a child of this visual container. */ public function getElementIndex(element:IVisualElement):int; /** * Changes the position of an existing visual element in the visual container. * * <p>When you call the <code>setElementIndex()</code> method and specify an * index position that is already occupied, the only positions * that change are those in between the elements's former and new position. * All others will stay the same.</p> * * <p>If a visual element is moved to an index * lower than its current index, the index of all elements in between increases * by 1. If an element is moved to an index * higher than its current index, the index of all elements in between * decreases by 1.</p> * * @param element The element for which you want to change the index number. * * @param index The resulting index number for the element. * * @throws RangeError - If the index does not exist in the child list. * * @throws ArgumentError - If the element parameter is not a child * of this visual container. */ public function setElementIndex(element:IVisualElement, index:int):void; //---------------------------------- // Visual Element swapping //---------------------------------- /** * Swaps the index of the two specified visual elements. All other elements * remain in the same index position. * * @param element1 The first visual element. * @param element2 The second visual element. */ public function swapElements(element1 :IVisualElement, element2 :IVisualElement):void; /** * Swaps the visual elements at the two specified index * positions in the visual container. All other visual * elements remain in the same index position. * * @param index1 The index of the first element. * * @param index2 The index of the second element. * * @throws RangeError If either index does not exist in * the visual container. */ public function swapElementAt(index1:int, index2:int):void; } }
这个接口使访问树变得容易了. 本质上, 这个接口为容器对外暴露有它哪些孩子提供了方法. 例如, FocusManager就是这样. 该接口使得 focus manager不依赖于Group 或是其它 Spark代码(除了这个接口), MX也不必增加太多代码. 我们讨论过要不要增加这些变异的(mutation) APIs,要不要MX也实现这些接口, 但我们认为这将有助有开发人员(框架开发人员) 实现所有容器(MX和Spark). 当我们看 DataGroup and SkinnableDataContainer 代码时, 你会发现他们并没有实现IVisualElementContainer接口, 尽管DataGroup有几个相似的 "只读的" 方法, 比如 numElements 和 getElementAt().
(smithfox注: 从Spark最终SDK中的代码可以验证, 如图)
IVisualElementContainer 持有 IVisualElements. IVisualElement 是可视化元素的一个新接口. 它包含了一些必要的属性和方法以使容器可以增加element. 他继承自 ILayoutElement 并增加了一些其它属性.
//////////////////////////////////////////////////////////////////////////////// // // ADOBE SYSTEMS INCORPORATED // Copyright 2003-2008 Adobe Systems Incorporated // All Rights Reserved. // // NOTICE: Adobe permits you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// package mx.core { /** * The IVisualElement interface defines the minimum properties and methods * required for a visual element to be laid out and displayed in a Spark container. */ public interface IVisualElement extends ILayoutElement { /** * The owner of this IVisualElement object. * By default, it is the parent of this IVisualElement object. * However, if this IVisualElement object is a child component that is * popped up by its parent, such as the drop-down list of a ComboBox control, * the owner is the component that popped up this IVisualElement object. * * <p>This property is not managed by Flex, but by each component. * Therefore, if you use the <code>PopUpManger.createPopUp()</code> or * <code>PopUpManger.addPopUp()</code> method to pop up a child component, * you should set the <code>owner</code> property of the child component * to the component that popped it up.</p> * * <p>The default value is the value of the <code>parent</code> property.</p> */ function get owner():DisplayObjectContainer; function set owner(value:DisplayObjectContainer):void; /** * The parent container or component for this component. * Only visual elements should have a <code>parent</code> property. * Non-visual items should use another property to reference * the object to which they belong. * By convention, non-visual objects use an <code>owner</code> * property to reference the object to which they belong. */ function get parent():DisplayObjectContainer; ...OTHER STUFF NOT DISCUSSED HERE... } }
(smithfox注: IVisualElement接口为什么是放在mx.core包内,确实有点怪, 但这是事实, 如图)
视觉元素的parent, 也就是容器, 直接负责布局. 视觉元素的owner是视觉元素的逻辑持有组件. 如果一个 Button在一个SkinnableContainer里, 它的parent是contentGroup而它的owner 是这个 SkinnableContainer.
请注意 parent 和 owner 属性类型是 DisplayObjectContainer 而不是 IVisualElementContainer. 这是因为在MX内, 这些属性就是
DisplayObjectContainer. 此外, 因为 parent 属性是继承自 Flash的 DisplayObject, 我们无法改变他. 我们曾讨论过为这个属性起个新名字, 但最后我们认为这样不值得.
(smithfox注: DisplayObjectContainer是flash.display.Sprite的父类)