【轉】從Flex中owner和parent的區別來說Flex API設計思路

譯文原地址:http://www.smithfox.com/?e=36

英文原地址:http://opensource.adobe.com/wiki/display/flexsdk/Gumbo+DOM+Tree+API

原譯文的圖片掛了,而那些圖片其實是詳細的說明,對於文章而言不可缺少的。所以轉載了譯文,並添加上原譯文丟失的圖片。(順便吐槽51CTO的水印擋住了文字,特地把某些圖片擴大了點……)


目的:

在Flex 4中有許多DOM(Document Object Model)樹。他們到底是怎麼組織和呈現的?

定義

圖形元素(graphic element) - 就象是矩形, 路徑, 或是圖片. 這些元素不是DisplayObject的子類; 但是它們還是需要一個DisplayObject來渲染到屏幕. (譯者注: "多個圖形元素可以只用一個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 樹.

本文中的全部圖的圖例如下:

wKiom1RXMuCSlM3GAAKdmOBWLVo576.jpg

背景:

當你用MXML創建應用程序時, 幕後發生了許多的事情,會將MXML轉換成Flash顯示對象. 後臺有三個主要因素: 皮膚,項渲染和顯示對象sharing. 前兩個對開發人員是非常重要的概念; 最後一個只需要框架開發人員關注, 但仍然比較重要.

皮膚:

當你初始化一個 Button, 其實創建了不止一個對象. 例如:

<s:Button />

在佈局樹中的結果是:

wKioL1RXNRSwp5vcAAEMhkSbc5s873.jpg

(注: 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屬性就可以得到一個ButtonSkin實例來訪問Rectangle 和Label

如要訪問Label, 你可以寫成:myButton.skin.getElementAt(2)或是 myButton.skin.labelDisplay.

由於labelDisplay是 Button 的 skin part, 所以你可可以直接寫成 myButton.labelDisplay.

同樣的原則也一樣適用在SkinnableContainer, SkinnableContainer是容器所以天然就有孩子, 但同時他們也是SkinnableComponent,所以也有一個皮膚以及來自皮膚的孩子.

(注: 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個孩子, 它的組件樹象這樣:

wKiom1RXOKKBLWmBAAHFdJpbZoE070.jpg

(注: TextBox 已經更名爲 Label)

但是, 這只是組件樹. 因爲皮膚的原因, Panel真正佈局樹是這樣的:

wKiom1RXOWKBxo9KAAIkTGYDtJA536.jpg

(注: 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) 或 myPanel.contentGroup.getElementAt(0) 或 

myPanel.skin.contentGroup.getElementAt(0).

所有 SkinnableComponent 都有 skin 屬性. 在 SkinnableContainer中組件的孩子實際上下推成爲skin的 contentGroup的孩子組件樹 指向編譯自MXML的語義樹. Panel 例子中, 只包括Panel 和他的孩子: 一個 button, 一個 label, 和一個checkbox. 由於皮膚, 佈局樹 是佈局系統所實際看到的樹.Panel 例子中,包括 這個panel, panel的皮膚, 以及這個皮膚的所有孩子(皮膚中的contentGroup的孩子).

佈局樹無需和所見的Flash顯示列表有什麼相關性. 這是因爲 GraphicElement 不是天然的顯示對象. 因爲考慮效率的原因, 他們最小化了顯示對象數目(譯者注: 多個GraphicElement可以在一個DisplayObject上渲染, 這樣DisplayObject的總數就可以大大減少).

IVisualElementContainer 定義了content APIs. 在Spark中, SkinGroup, 和 SkinnableContainer 實現了這個接口,持有着可視化元素. 爲保持一致性, MX的 Container 也實現了這個接口, 不過只是對addChild(),numChildren 等函數的封裝....

這個接口使訪問樹變得容易了. 本質上, 這個接口爲容器對外暴露有它哪些孩子提供了方法. 例如,FocusManager就是這樣. 該接口使得 focus manager不依賴於Group 或是其它 Spark代碼(除了這個接口), MX也不必增加太多代碼. 我們討論過要不要增加這些變異的(mutation) APIs,要不要MX也實現這些接口, 但我們認爲這將有助有開發人員(框架開發人員) 實現所有容器(MX和Spark). 當我們看 DataGroup and SkinnableDataContainer 代碼時, 你會發現他們並沒有實現IVisualElementContainer接口, 儘管DataGroup有幾個相似的 "只讀的" 方法,

比如 numElements 和 getElementAt().

IVisualElementContainer 持有 IVisualElementsIVisualElement 是可視化元素的一個新接口. 它包含了一些必要的屬性和方法以使容器可以增加element. 他繼承自 ILayoutElement 並增加了一些其它屬性.

視覺元素的parent, 也就是容器, 直接負責佈局. 視覺元素的owner是視覺元素的邏輯持有組件. 如果一個 Button在一個SkinnableContainer裏, 它的parent是contentGroup而它的owner 是這個 SkinnableContainer.

請注意  parent 和 owner 屬性類型是 DisplayObjectContainer 而不是 IVisualElementContainer. 這是因爲在MX內, 這些屬性就是 
DisplayObjectContainer. 此外, 因爲 parent 屬性是繼承自 Flash的 DisplayObject, 我們無法改變他. 我們曾討論過爲這個屬性起個新名字, 但最後我們認爲這樣不值得.

(譯者注: 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組件宣佈誰是他的可視化孩子的方式.Group 和 SkinnableContainer 都實現了這個接口. 另外, MX的 Container 也實現了這個接口. 但那只是對顯示列表APIs的一種封裝, IVisualElementContainer 提供了唯一的,一致的訪問容器孩子的方法.

在Spark中, SkinnableContainer 仍然有DisplayList API(譯者注: 就是在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中, 有兩個主要的容器類型: 一個容納可視化元素,另一個容納數據項. DataGroup 和 SkinnableDataContainer 用來容納數據項. Group 和 SkinnableContainer 用來容納可視化元素. 一個數據容器能容納任何東西, 但特別是用來容納非可視元素 (比如.-真正的數值). 有關數據容器重要的一點是它們支持 項渲染,就是將數據項轉換爲可視元素.

項渲染

DataGroup 有能力將隨意的非可視化元素呈現到屏幕. 因此, 項渲染器正好可以加到佈局樹中. 某些情況下, 甚至於可視化元素,比如 UIComponents 和 GraphicElements, 也被包裝成項渲染器. 爲向開發人員展現這個設計思路,我們考慮一下以下幾個可選方案:

  1. DataGroup 和 SkinnableDataContainer 設計成葉子節點, 他們的實際可視化孩子不能被訪問

  2. DataGroup 和 SkinnableDataContainer 實現IVisualElementContainer接口. 當問屏幕上有幾個可視化元素時, 我們只返回當前屏幕正在被渲染的那些元素. Mutation APIs RTE(RuntimeException).

  3. DataGroup 和 SkinnableDataContainer 實現IVisualElementContainer接口. 當問屏幕上有幾個可視化元素時, 我們返回所有項個數. 如果用戶訪問一個還未曾被渲染過的項時, 我們就創建並且渲染.

我們決定向DataGroup增加 "只讀"的 element APIs, 象numElementsgetElementAt(), 和 getElementIndex(). 還有另一個API,getItemIndicesInView()決定哪些數據項在屏幕顯示.

象MX一樣, 項渲染器的 owner 屬性總是和組件的 owner屬性是一樣的. 項渲染器的 parent 屬性負責渲染.

這兩個圖顯示了項渲染的運行.

wKioL1RXO4uAOJ0RAAGvdJw_Wqc820.jpg

你會注意到DataGroup 在組件樹中沒有孩子. 這是因爲它被看着是渲染數據的葉子節點. 

下圖是DataGroup的佈局樹例子:

wKiom1RXO5TDdpUkAAFxWFom22I761.jpg

(注: 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 指向了負責它的組件.

需要做的一些變化(譯者注: 這個是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 of SkinnableComponent and Skin. 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. 這些新定義的類將會實現IVisualElement 和 IVisualElementContainer接口. 因爲這些類是新定義的, 它們將會覆蓋老版本的基類. 另一個解決方案是隻更新組件工具包而不再支持老的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].


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章