`
piperzero
  • 浏览: 3473957 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

从Flex中owner和parent的区别来说Flex API设计思路

 
阅读更多

这篇文章是拷贝过来的,读完确实让我对Flex了解的更通透了,文章比较长,请耐心看完。

英文原文: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. 这个例子中,PanelLabel都在组件树中, 但是Panel的皮肤却不是.

布局树- 布局树呈现了运行时的布局. 在这个树中, 父亲负责呈现和布局对象, 孩子则是被布局的视觉元素. 举个简单例子, 一个Panel包含了一个Label.这个例子中,PanelLabel都在布局树中,同样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实例来访问RectangleLabel. 如要访问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可以访问他们. 这些contentAPIs很像flashDisplayObjectContainer的 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)ormyPanel.contentGroup.getElementAt(0)ormyPanel.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定义了contentAPIs. 在Spark中,Skin,Group, 和SkinnableContainer实现了这个接口,持有着可视化元素. 为保持一致性, MX的Container也实现了这个接口, 不过只是对addChild(), numChildren, 等函数的封装....

这个接口使访问树变得容易了. 本质上, 这个接口为容器对外暴露有它哪些孩子提供了方法. 例如,FocusManager就是这样. 该接口使得 focus manager不依赖于Group或是其它 Spark代码(除了这个接口), MX也不必增加太多代码. 我们讨论过要不要增加这些变异的(mutation) APIs,要不要MX也实现这些接口, 但我们认为这将有助有开发人员(框架开发人员) 实现所有容器(MX和Spark). 当我们看 DataGroup and SkinnableDataContainer 代码时, 你会发现他们并没有实现IVisualElementContainer接口, 尽管DataGroup有几个相似的 "只读的" 方法, 比如numElementsgetElementAt().

(smithfox注: 从Spark最终SDK中的代码可以验证, 如图)



IVisualElementContainer持有IVisualElements.IVisualElement是可视化元素的一个新接口. 它包含了一些必要的属性和方法以使容器可以增加element. 他继承自ILayoutElement并增加了一些其它属性.

(smithfox注:IVisualElement接口为什么是放在mx.core包内,确实有点怪, 但这是事实, 如图)


视觉元素的parent, 也就是容器, 直接负责布局. 视觉元素的owner是视觉元素的逻辑持有组件. 如果一个 Button在一个SkinnableContainer里, 它的parent是contentGroup而它的owner 是这个SkinnableContainer.

请注意 parentowner属性类型是DisplayObjectContainer而不是IVisualElementContainer. 这是因为在MX内, 这些属性就是
DisplayObjectContainer. 此外, 因为parent属性是继承自 Flash的DisplayObject, 我们无法改变他. 我们曾讨论过为这个属性起个新名字, 但最后我们认为这样不值得.

(smithfox注: DisplayObjectContainer是flash.display.Sprite的父类)

MX 组件

MX 组件和有上面有着相同的概念, 但是大部分隐藏在后台. Spark组件则因为皮肤化就变得更加透明.

一个MX button有一个孩子, 就是TextField. 这个孩子是直接通过addChild() (没有皮肤)方法加到Button的. 例如, 这个Button的TextField就是Button的孩子. 所以如果你查看Button的孩子, 他将返回给你这个TextField. 如果你问这个TextField父亲, 他将返回这个Button.

在Spark中, 一个Button只有一个孩子, 皮肤对象. 皮肤对象包含了一个Label. 如果你问Button的显示对象孩子, 它将告诉你它有一个孩子:皮肤. 如果你想确认Button皮肤的孩子, 你应该调用皮肤对象中的方法.

容器有些难懂,它包括了组件孩子和皮肤孩子. 在MX中, Panel的显示列表包含了皮肤孩子和一个叫"contentPane"的组件孩子. panel的所有组件孩子都放到这个contentPane. 这和Spark非常象; 然而, 在MX中对开发人员隐藏了太多细节. 如果你问Panel的显示列表孩子, 它其实对你撒谎了, 它返回你这个contentPane孩子(Panel的组件孩子). 为访问皮肤孩子, 可以通过rawChildren属性返回孩子列表. 如果你问Panel的组件孩子的它的父亲是谁, 它会告诉你是这个panel, 但实际上他的父亲应该是contentPane.

在Spark中,IVisualElementContainer接口可以让你访问孩子. 这也是Spark组件宣布谁是他的可视化孩子的方式.GroupSkinnableContainer都实现了这个接口. 另外, MX的Container也实现了这个接口. 但那只是对显示列表APIs的一种封装,IVisualElementContainer提供了唯一的,一致的访问容器孩子的方法.

在Spark中,SkinnableContainer 仍然有DisplayList API(smithfox注: 就是在Flex 3中的操作children的函数, 比如addChild). 但是, 但是如果你想试图通过这些API操作 DisplayList, 我们将抛出一个运行时异常. 当你访问numChildren或是getChildAt()函数, 不像在MX中, Spark会如实地返回他的显示列表. 当你调用SkinnableContainer的 "content API" (numElements, getElementAt()) ,它将返回它的组件孩子 (contentGroup的实际的所有孩子). 要访问皮肤孩子 (就象MX组件中的"rawChildren"), 你需要调用skin对象的方法. 当你问Panel组件的孩子问谁是它的parent, 它会返回contentGroup(不象MX返回这个Panel). 但是有另外一个属性会返回Panel, 那就是owner. owner属性MX也有, 但是在MX中和它parent属性返回的是一样. 在Spark中, owner 和 parent则指向了不同的对象.

数据项

在Spark中, 有两个主要的容器类型: 一个容纳可视化元素,另一个容纳数据项.DataGroupSkinnableDataContainer用来容纳数据项.GroupSkinnableContainer用来容纳可视化元素. 一个数据容器能容纳任何东西, 但特别是用来容纳非可视元素 (比如.-真正的数值). 有关数据容器重要的一点是它们支持 项渲染,就是将数据项转换为可视元素.

项渲染

DataGroup有能力将随意的非可视化元素呈现到屏幕. 因此, 项渲染器正好可以加到布局树中. 某些情况下, 甚至于可视化元素,比如UIComponentsGraphicElements, 也被包装成项渲染器. 为向开发人员展现这个设计思路,我们考虑一下以下几个可选方案:

  1. DataGroup 和 SkinnableDataContainer 设计成叶子节点, 他们的实际可视化孩子不能被访问
  2. DataGroup 和 SkinnableDataContainer 实现IVisualElementContainer接口. 当问屏幕上有几个可视化元素时, 我们只返回当前屏幕正在被渲染的那些元素. Mutation APIs RTE(RuntimeException).
  3. DataGroup 和 SkinnableDataContainer 实现IVisualElementContainer接口. 当问屏幕上有几个可视化元素时, 我们返回所有项个数. 如果用户访问一个还未曾被渲染过的项时, 我们就创建并且渲染.

我们决定向DataGroup增加"只读"的 element APIs, 象numElements,getElementAt(), 和getElementIndex(). 还有另一个API,getItemIndicesInView()决定哪些数据项在屏幕显示.

象MX一样, 项渲染器的owner属性总是和组件的 owner属性是一样的. 项渲染器的parent属性负责渲染.

这两个图显示了项渲染的运行.


你会注意到DataGroup组件树中没有孩子. 这是因为它被看着是渲染数据的叶子节点. 下图是DataGroup的布局树例子:


(注: TextBox 已更名为 Label)

上面例子中, 字符串不是一个可视化的元素并且需要一个项渲染器. 创建一个项渲染器包装这个字符串对象. 它的owner属性就是DataGroup. 因为设置了一个 itemRendererFunction 对象,所以 Employee Object 和其它的字符串一样都会得到处理.

用例:

开发人员通常只和组件树打交道. 布局和效果就像FocusManager一样和布局树打交道. 只有像Group的 DisplayObject的sharing code这样的底层的代码才和显示树打交道.

API 说明

public interface IVisualElementContainer {
    public function get numElements():int;
    public function getElementAt(index:int):IVisualElement;
    public function getElementIndex(element:IVisualElement):int;
    public function addElement(element:IVisualElement):IVisualElement;
    public function addElementAt(element:IVisualElement, index:int):IVisualElement;
    public function removeElement(element:IVisualElement):IVisualElement;
    public function removeElementAt(index:int):IVisualElement;
    public function setElementIndex(element:IVisualElement, index:int):void;
    public function swapElements(element1:IVisualElement, element2:IVisualElement):void;
    public function swapElementsAt(index1:int, index2:int):void;
}
 
public interface IVisualElement extends ILayoutElement {
    owner:DisplayObjectContainer;
    parent:DisplayObjectContainer;
    ...other stuff not discussed here...
}
 
public class UIComponent implements IVisualElement {
    owner:DisplayObjectContainer;
    parent:DisplayObjectContainer;
    ...other stuff...
}
 
public class GraphicElement implements IVisualElement {
    owner:DisplayObjectContainer;
    parent:DisplayObjectContainer;
    ...other stuff...
}
 
[DefaultProperty("content")]
public class Group extends GroupBase implements IVisualElementContainer {
    [write-only] mxmlContent:Array;
    layout:ILayout;
    public function get numElements():int;
    public function getElementAt(index:int):IVisualElement;
    public function getElementIndex(element:IVisualElement):int;
    public function addElement(element:IVisualElement):IVisualElement;
    public function addElementAt(element:IVisualElement, index:int):IVisualElement;
    public function removeElement(element:IVisualElement):IVisualElement;
    public function removeElementAt(index:int):IVisualElement;
    public function setElementIndex(element:IVisualElement, index:int):void;
    public function swapElements(element1:IVisualElement, element2:IVisualElement):void;
    public function swapElementsAt(index1:int, index2:int):void;
}
 
public class Skin extends Group {
}
 
public class SkinnableComponent extends UIComponent {
    function get skin():Skin;
    [CSS] function set skinClass:Class;
}
 
[DefaultProperty("content")]
public class SkinnableContainer extends SkinnableContainerBase implements IVisualElementContainer {
    [write-only] mxmlContent:Array;
    public function get numElements():int;
    public function getElementAt(index:int):IVisualElement;
    public function getElementIndex(element:IVisualElement):int;
    public function addElement(element:IVisualElement):IVisualElement;
    public function addElementAt(element:IVisualElement, index:int):IVisualElement;
    public function removeElement(element:IVisualElement):IVisualElement;
    public function removeElementAt(index:int):IVisualElement;
    public function setElementIndex(element:IVisualElement, index:int):void;
    public function swapElements(element1:IVisualElement, element2:IVisualElement):void;
    public function swapElementsAt(index1:int, index2:int):void;
    [SkinPart] contentGroup:Group;
}
 
public class Container extends UIComponent implements IVisualElementContainer {
    public function get numElements():int;
    public function getElementAt(index:int):IVisualElement;
    public function getElementIndex(element:IVisualElement):int;
    public function addElement(element:IVisualElement):IVisualElement;
    public function addElementAt(element:IVisualElement, index:int):IVisualElement;
    public function removeElement(element:IVisualElement):IVisualElement;
    public function removeElementAt(index:int):IVisualElement;
    public function setElementIndex(element:IVisualElement, index:int):void;
    public function swapElements(element1:IVisualElement, element2:IVisualElement):void;
    public function swapElementsAt(index1:int, index2:int):void;
}
 
 
[DefaultProperty("dataProvider")]
public class DataGroup extends UIComponent {
    dataProvider:IList;
    itemRenderer/itemRendererFunction;
    layout:ILayout;
 
    public function get numElements():int;
    public function getElementAt(index:int):IVisualElement;
    public function getElementIndex(element:IVisualElement):int;
 
    public function getItemIndicesInView():Vector.;
}
 
[DefaultProperty("dataProvider")]
public class SkinnableDataContainer extends SkinnableContainerBase {
    dataProvider:IList;
    layout:ILayout;
    itemRenderer/itemRendererFunction;
    [SkinPart] dataGroup:DataGroup;
}

//遍历这些树的样例代码:

public function walkTree(element:IVisualElement, proc:Function):void
{
    proc(element);
    if (element is IVisualElementContainer)
    {
        var visualContainer:IVisualElementContainer = IVisualElementContainer(element);
        for (var i:int = 0; i < visualContainer.numElements; i++)
        {
            walkTree(visualContainer.getElementAt(i));
        }
    }
         
}
 
public function walkLayoutTree(element:IVisualElement, proc:Function):void
{
    proc(element);
    if (element is SkinnableComponent)
    {
        var skin:Skin = SkinnableComponent(element).skin;
        walkTree(skin);
    }
    else if (element is IVisualElementContainer)
    {
        var visualContainer:IVisualElementContainer = IVisualElementContainer(element);
        for (var i:int = 0; i < visualContainer.numElements; i++)
        {
            walkTree(visualContainer.getElementAt(i));
        }
    }
    // expand this to MX and IRawChildrenContainer?
}
 
public function walkUpTree(element:IVisualElement, proc:Function):void
{
    while (element!= null)
    {
        proc(element);
        element = element .owner;
    }
 
}
 
public function walkUpLayoutTree(element :IVisualElement, proc:Function):void
{
    while (element != null)
    {
        proc(element );
        element = element .parent;
    }

更多关于 parent/owner

有一种看待parent属性的方法是: "谁布局我". 如果你是一个DisplayObject, 这同时也对应着你的物理显示列表parent. (GraphicElements这里做了一点伪装,因为他们并不是显示对象,但也同一个概念).

owner属性的用途:

  • 它能告诉你在组件树(或是SkinnableContainer中的elements)中谁是你的父亲
  • 它能告诉项渲染器哪个数据容器负责他们
  • 它还用在弹出窗口, 象DateField, 它告诉你谁在负责这个弹出窗口.

看待owner属性的方式就是: 一个元素的owner指向了负责它的组件.

需要做的一些变化(smithfox注: 这个是Flex 4的设计规格,所以应该是说给adobe开发人员听的)

GraphicElement增加parentowner属性. 在适当的地方将这些属性(项渲染器和SkinnableContainer)衔接起来.

建议主要还是创建和实现这些接口.

已经分开了Group和DataGroup. 这就可以按完全独立的规范性工作项目来运作.

最后, 还需要做些虚拟化规范相关的工作, 以实现怎样呈现这样已经渲染过的元素.

重要的/有争议的 观点:

  • 这些不同的DOM树有些让人迷惑, 我们需要向Flex开发人员做些明确的解释.
  • 我们引入owner属性是因为这样人们可以遍历逻辑DOM树. 我们引入parent属性是因为这样人们可以遍历布局DOM树. 我们考虑过是否不需要parent属性,因为它的类型是DisplayObjectContainer, 在将来的某个时候, parent 节点不必一定是DisplayObjectContainers. 但现在还是,parent这个名字比其它名字要适合一些. 如果我们决定使容器变成非DisplayObjects, 那时我们可能会贯彻到底, 将所有的DisplayObject都变成是可选的l.
  • Walking the layout tree requires knowledge ofSkinnableComponentandSkin. This means Mustella (or other places) will need to bring these classes in (or treat them as untyped).
  • MX也实现IVisualElementContainer接口.
  • owner属性看上去有3个不同的用途.
  • Scroller也实现IVisualElementContainer接口以宣布它有一个孩子. 我们考虑过为"decorators(装饰)" 创建一个单独的接口, 但我们倾向更通用这样也能处理HDividedBoxes. 这些"getter"方法将能在Scroller使用, 其它的就抛出运行时异常.
  • 我们考虑过在gumbo容器中支持flash原生display objects, 但最后还是否决了.
  • 我们需要支持新的组件工具包和那些用老的组件工具包制作的swc. 一种解决方案是always link in UIMovieClip and the other FCK classes. 这些新定义的类将会实现IVisualElementIVisualElementContainer接口. 因为这些类是新定义的, 它们将会覆盖老版本的基类. 另一个解决方案是只更新组件工具包而不再支持老的scw. 我们需要更多的PM的决定; 但是不管怎么样, 这样类是需要更样的.
  • 我们同时有多套Flash DisplayObjectContainer APIs, 他们分别继承自Group/DataGroup/SkinnableComponent (addChild(), getChildAt(), 等). 为处理这个问题, 所有mutation(变异的) APIs调用 (addChild(), removeChild(), swapChildren(), 等...) 都将抛出运行时异常. 只有允许调用"getters" 类的方法. 我们也试图在正常API(getChildAt, numChildren, etc...)调用时也抛出异常, 但会有架构方面的问题, 比如UIComponent的 removeChildAt 方法就依赖于这些API.如果这原来是一个优先事项, 我们可以在这些方法中都加入运行异常并且提供新的方法, 比如 $getChildAt_SkinnableComponent之类. 然后我们在这些新API的基础上改动所有framework的代码. 这样做又有新的问题:[Child APIs vs. Item APIs].
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics