Flex 中的類體系結構
Flex 包含大量的類,因而無法在本文中全部描述。不過,仔細查看一些類對了解 Flex 對象模型是非常有用的。圖 1 顯示了核心的類體系結構:
圖 1. Flex 的核心類體系結構
最頂層的類是 DisplayObject
,Flex 將它添加到 Flash Player 的 Stage 對象或通用顯示列表。InteractiveObject
處理用戶交互,包括鍵盤和鼠標事件。DisplayObjectContainer
允許您添加子組件,并在特定范圍內移動它們。Sprite
和 FlexSprite
是不需要時間線的基類,您可以通過擴展它們編寫定制類。不過,這個類集合中的主角是 UIComponent
類,它為您提供編寫定制組件所需的基礎框架。您通常通過擴展這個類來編寫定制代碼。
Flex 將您創建的所有組件作為最終由 Flash Player 處理的通用顯示列表的子組件。Flex 的體系結構隱藏了這些細節,但它提供連接到事件監聽器的鉤子,讓您能夠在高級階段處理事件。Flash Player 的事件分發機制非常健壯,能夠處理上萬個事件。
到目前為止,我討論了 Flex 給 Web 開發帶來的主要好處、它的基礎體系結構和一些核心類。但是它們對開發人員而言只不過是序幕而已:編寫代碼和構建解決方案。用 Flex 創建簡單的組件是很容易的。假設您需要創建兩個文本輸入框,當向其中之一輸入文本時,將顯示用戶在兩個文本框中輸入的文本。您不需要編寫任何關于事件處理的代碼就可以創建這樣的組件。您將創建的簡單 UI 類似于圖 2。
圖 2. 兩個交互文本框
清單 1 顯示了創建這個簡單的應用程序所需的幾行代碼:
清單 1. 創建一個簡單的 Flex UI
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical">
<mx:TextInput id="box1" text="{box2.text}">
<mx:TextInput id="box2" text="{box1.text}">
</mx:Application>
這段代碼非常簡單干凈,但不要僅從表面看:它能完成很多事情。如果您希望查看 Flex 替您完成的所有事情,那么使用編譯器標記 keep-generated-actionscript=true
編譯這段代碼。Application
容器(查看 mx:Application 標記)和 TextInput
控件對您隱藏了許多復雜的代碼。只要您的老板同意您擴展、重用和聚合已經存在的 Flex 組件,這就是件好事。不過,當您從頭構建自己的組件時,事情就會迅速變得復雜起來。構建定制組件表明 Flex 可以為您自動完成很多事情;您欣賞的許多 Flex 特性并不包含在精簡的 Flex 組件中。
創建定制組件的理由
隨著 Flex 的成熟,我相信您將看到越來越多的開發人員開始編寫自己的定制 Flex 組件,主要原因有兩個:
- 大部分 Flex 組件都是迎合大眾的需求,現有的控件可能不能滿足您的特定需求。修改和擴展現有的組件能夠幫助您填補某些空缺,但是如果您要從根本上改變特定組件的行為,那么就必須編寫自己的組件。
- 許多新的可視化標準要求您以新的格式表示數據,這些格式沒有得到充分支持或不存在。如果您希望在應用程序中利用這些新格式的優勢,就必須提供這種功能。
Flex 組件的生命周期
如果您打算構建定制的組件,那么理解 Flex 組件的生命周期是至關重要的。Flex 生命周期的驅動力是 LayoutManager
。看看節選自這個 Flex API 的描述:
“LayoutManager 是 Flex 的度量和布局戰略背后的引擎。布局通過 3 個階段完成:提交、度量和布局。”
第一個階段是提交,該階段在 Flex 使用 addChild()
或它在 DisplayObject 類中的變體之一向主顯示列表(此后稱為 “階段”)添加組件時發生。注意,Flex 沒有向這個列表直接添加組件;在這發生之前需要經過幾個中間步驟。首先,Flex 驗證組件的所有屬性。LayoutManager
調用父對象上的 validateProperties()
,然后遍歷調用每個子對象。這稱為自上而下排序。如果存在 “臟” 屬性,LayoutManager
允許您在驗證發生之前提交它們。您可以通過調用 UIComponent
中的保護方法 commitProperties()
進行提交。定制組件必須在驗證發生之前覆蓋這個方法以提交屬性。通過查看 Label
控件能夠幫助您更好地了解發生了什么。標簽是在應用程序中呈現一行文本的文本字段。您可以在一個標簽中更改多個值,包括字體、字體大小、字體顏色和文本等。要在標簽中更改與字體相關的東西,您必須提供一個新的字體上下文和一個新的與新字體對應的 UITextField
;這能幫助確保新字體能夠正確呈現。
Flex 如何處理提交更改
現在,考慮一下當應用程序更改標簽的字體時會發生什么。記住,Flex 的 UI 依賴于異步處理,因此任何更改都不會立即生效。在更改字體時,您可以同時創建一個新的 UITextField
,或者存儲一個新的字體上下文和一個表明字體已經更改的標記。當下次呈現該組件時(通常發生在下一次屏幕刷新),您就可以應用新的字體。檢查表明字體狀態變更的標記是否改變,然后創建一個對應的 UITextField
。有可能在兩次屏幕刷新之間多次更改這些屬性,但這讓變更處理的效率下降。更好的策略是等待下一次屏幕刷新,然后再應用更改。注意,終端用戶不會發現任何變化,因為屏幕的刷新頻率下降了。這是針對任何異步、事件驅動 UI 的典型用例。應用程序存儲需要應用的變更、對這些變更進行排序、然后在適當時間應用它們。您可能要問,什么時候才是最佳時間。這正是 LayoutManager
處理的任務。當您收到一個對組件的 commitProperties()
的回調時,就可以確定這是再次呈現該組件之前的最后一個調用;在這個調用之后 對屬性所做的任何變更都不會出現在即將發生的屏幕刷新中。這些變更必須在下一次屏幕刷新和調用 commitProperties()
時才被應用。
這些都好理解,但您可能很想知道如何告知 LayoutManager
根據組件執行驗證。假設某人更改了標簽的字體,然后您存儲了新的字體和相關標記,現在您需要讓 LayoutManager
再次調用驗證階段。您可以為相應的組件調用一個稱為 invalidateProperties()
的方法來實現驗證。現在,我們看看更改標簽的文本所需的代碼。清單 2 顯示了如何處理驗證階段(為了保持簡潔,省略了一些代碼)。
清單 2. 處理
public function set text(value:String): void {
_text = value;
textChanged = true
invalidateProperties();
}
驗證階段
注意,_text
是臨時存儲新的文本值的字段,而 textChanged
是用來標記更改的標記。此外還要注意為了通知 LayoutManager
開始驗證階段而進行的 invalidateProperties()
調用。清單 3 顯示了 Label
類的 commitProperties()
中的對應代碼片段(注意,為了保持簡潔我省略了一些代碼):
清單 3. 使用 commitProperties
override protected function commitProperties():void{
if (textChanged) {
textField.text = _text;
textChanged = false
}
這個被覆蓋的方法演示了當標記被設置為 true 時,如何更新 UITextField
(textField),以及如何將該標記重新設置為 false 以在未來進行標記。如果您希望使用 Flex 創建定制控件,那么這是必須理解的關鍵點。在清單 2 和清單 3 中討論的驗證階段展示了 Flex 呈現引擎的 3 大基礎支柱之一。這也是最容易被忽視的階段,有許多高級的組件甚至沒有驗證階段。性能考慮是忽略驗證階段的部分原因。在發生更改時就立即應用它們一般不會引起錯誤,但是無目的、不必要地多次應用變更會導致 Flex 應用程序的性能問題(一般情況是小問題,但也可能是大問題),理解這點非常重要。
開發人員通常在兩種情況下編寫組件。第一種情況是,編寫一個與應用程序用例緊密結合的組件。通常,這種應用程序還不適合重用,因此可以適當犧牲性能以避免組件過于復雜。第二種情況是,編寫適合在框架級別使用或由其他開發人員使用的組件。對于這種情況,盡可能提高組件的性能是值得的。一般在第二種情況中才需要認真處理驗證階段。
度量階段是另一個重要的 Flex 階段,它比提交階段更常用。在組件通過了驗證之后,LayoutManager
將繼續度量組件,使它能夠顯示在特定上下文中。例如,LayoutManager
可能將組件顯示為特定容器的子容器。您需要使用度量階段,因為它幫助 LayoutManager
確定父容器是否包含滾動條。一些組件可以根據其子組件正常顯示所需的空間調整自身的大小。考慮 VBox 示例,它是一個垂直排列其子容器的容器。如果您沒有給 VBox 分配 height
或 width
值,它就在每次添加子容器時重新調整大小。它的 height
足以包含所有垂直排列的子容器,除非您設置某些限制。Flex 包含一個稱為 measure()
的保護方法,您可以用它度量組件。LayoutManager
在適當的時間(一般在下一次屏幕刷新之前)調用這個方法,您需要在這個時間點度量組件并使用 measuredWidth
和 measuredHeight
設置 height
和 width
。然后,父容器使用這些屬性度量它本身。
首先度量子容器
與驗證階段相反,度量階段是自下而上的。這是因為必須在度量父容器之前度量子容器。這確保當父容器收到度量調用時,已經度量了它的子容器。到目前為止,一切進展順利。但是,現在假設您顯式地指定了組件的 height
和 width
。Flex 將這些存儲的值指定為 explicitWidth 或 explicitHeight 屬性。Flex 將考慮您在這種情況下指定的 width
和 height
值。不過,有時給容器設置顯式度量是不明智的,因為您可能不能預先知道容器的所有子容器的總大小。 對于這種情況,Flex 給超出指定界限的子容器添加一個滾動條。注意,這些滾動條僅出現在擴展 Container
類的容器上。對于其他控件,滾動條可能出現,或者 Flex 會剪切掉一部分內容區域(稱為內容切除)以保持您指定的大小。
顯式地設置屬性值
顯式地設置 width
和 height
值將導致一個有趣的問題。如果您編寫一個包含其他組件的組件并希望度量容器的大小,那么了解每個子容器的 width
和 height
將有幫助。您可能很想知道怎樣才能知道特定組件的大小是被顯式地指定的,或通過覆蓋度量方法進行度量。回想一下,度量大小包含在 measuredWidth 和 measuredHeight 等屬性中,而顯式定義的大小包含在 explicitWidth 和 explicitHeight 中。僅有一對度量包含實數值,而另一對度量將包含 Not-a-Numbers (NaN)。Flex 通過提供兩個方法解開了這個謎團:getExplicitOrMeasuredWidth()
和 getExplicitOrMeasuredHeight()
。您僅需調用這些方法,而不必擔心 height
和 width
值是否被度量或顯示地設置。當一個組件被度量之后,就需要調用 setActualSize()
方法,它將設置 width
和 height
屬性。只要顯式地設置了 height
和 width
,同時也就設置了這些屬性。清單 4 演示了使用顯式的 width
和 height
值創建文本輸入組件有多么簡單。清單 4 接收 4 個屬性:
清單 4. 輸入 width
和 height
<mx:TextInput width="200" height="45"/>
這個例子包含 4 個屬性:width
、height
、explicitWidth
和 explicitHeight
。注意,measuredWidth
和 measuredHeight
保持為 NaN。
現在,看一看清單 5,其中沒有設置任何顯式的 height
或 width
值:
清單 5. 組件度量
<mx:TextInput />
在這個例子中,組件本身提供了度量。就像在 Flex 框架的另一個地方一樣,這里的 “默認” width
和 height
值被設置為 measuredWidth
和 measuredHeight
。顯式的值仍然保留為 NaN。無論哪種情況,getExplicitOrMeasuredWidth()
和 getExplicitOrMeasuredHeight()
都返回正確的值。類似地,Flex 為 explicitMinWidth
或 measuredMaxWidth
設置最小的大小,以為組件提供最小和最大的界限。當組件的大小超出 maxWidth
指定的界限時,滾動條將顯示當前視圖區域內看不到的內容。注意,當設置了顯式大小時,LayoutManager
將不調用 measure()
。這是有意義的,因為當您顯式地定義了大小之后,就沒有必要再次度量組件。
調用 invalidateSize() 將告知 LayoutManager
初始化度量階段。LayoutManager
包含 3 個不同的隊列:invalidatePropertiesQueue
、invalidateSizeQueue
和 invalidateDisplayListQueue
。這些隊列與在生命周期的某個點上調用 invalidateProperties()
、invalidateSize()
和 invalidateDisplayList()
的組件對應。然后,LayoutManager
從每個隊列中處理對應的組件,并調用每個組件的 validateProperties()
、validateSize()
和 validateDisplayList()
方法。然后,這些方法中的默認實現 UIComponent
將調用 commitProperties()
、measure()
和 updateDisplayList()
。您可以覆蓋這些方法以編寫定制的邏輯。
度量組件可能還涉及到度量文本。然而,文本的行為與其他控件大不相同。度量文本要求您考慮升序、降序和行距等。幸運的是,Flex 提供的一個實用程序簡化了這一過程。UIComponent
公開稱為 measureText()
和 measureHtmlText()
的方法幫助您度量文本控件。清單 6 顯示了如何度量文本字段:
清單 6. 度量文本字段
package components {
import flash.text.TextLineMetrics;
import mx.core.UIComponent;
public class MyLabel extends UIComponent {
private var _text : String;
public function set text(value :
String) : void {
_text = value;
invalidateSize()
}
public function get text() : String {
return _text
}
override protected function measure(): void {
if (!_text) return super.measure();
//measure text here!
var textLineMetrics : TextLineMetrics =
super.measureText(_text);
super.setActualSize(textLineMetrics.width,
textLineMetrics.height);
}
這段代碼創建了一個簡單的擴展 UIComponent
的 MyLabel
類,因此您僅需關注度量階段。注意,您將在設置文本時調用 invalidateSize()
,它通知 LayoutManager
在下一次屏幕刷新期間將組件添加到它的 invalidateSizeQueue()
。然后,LayoutManager
調用組件的 validateSize()
方法,它是在 UIComponent
中定義的。最后,validateSize()
調用您曾經覆蓋的 measure()
方法。這個調用使您能夠設置控件的大小。您可以簡單地將 width
設置為 200,并將 height
設置為 45,然后接受最后的結果。MyLabel
類的所有實例將共享相同的大小,這似乎不是好事情,因為它首先就不支持使用類似于 Flex 的健壯 UI 開發工具。但是使用了固定大小時,您就不需要覆蓋 measure()
方法,甚至可以在將組件添加到其父組件時定義 width
和 height
。清單 7 顯示了度量標簽控件的 2 個變體。
清單 7. 度量標簽
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical" xmlns:components="article1.components.*">
<components:MyLabel id="label1"/>
<components:MyLabel ="label2"width="200" height="50"/>
</mx:Application>
在這個例子中,將調用 label1
的 measure()
方法,因為您沒有顯式地定義它的大小。然而,label2
的 measure
沒有被調用,因為您顯式地定義了控件的大小。注意,Flex 的 Label
類中的 measure()
實現的功能不僅僅是調用 measureText()。您需要處理左邊、右邊、頂部和底部的邊框填充。但基本原理是一樣的。
度量階段是 Flex 組件的生命周期中的重要階段。盡管這是一個重要的階段,但您很少需要關注它,除非您從頭開始構建組件。大部分定制的 Flex 組件可能忽略了這個階段,并將它留給 Flex 處理。Flex 在處理類似的瑣碎事情方面表現非常出色,讓您從乏味的度量任務中解脫出來。不過,如果您需要為 Flex 框架編寫一個新的組件,那么就需要精通這個階段。
布局階段是 Flex 框架的 3 大生命周期階段中的最后一個。您需要在這個階段上花費大量時間,以致于很難將其看作是一個階段。所有圖形工作都發生在這個階段,包括皮膚和藝術性增強等。
假設您擁有一個經過適當驗證和度量的組件。下一步就是考慮它的布局。布局階段處理組件的許多方面。例如,您需要在這個階段處理所有背景圖像。您還需要打包在這個階段中需要使用皮膚的所有組件。在這個階段中,還可以將組件移動到理想位置。再次以 VBox 為例,這是一個垂直排列的容器。當您向它添加子容器時,您必須以垂直的方式排列它們。這種排列發生在布局階段。
Flash 坐標系的工作原理
我將引導您了解如何使用 Flex 將子容器移動到 VBox 中,但我們首先溫習一下 Flash Player 的坐標系。任何組件的左上角都由 x 和 y 值表示。每個組件的 x 和 y 值都與它的直接父組件相關。x 軸在右邊為正值,但是 y 軸在下方為正值(這與傳統的上方為正值相反)。因此,如果您在 VBox 中放置了一個標簽,其 x 和 y 值分別為 30 和 40,標簽的左上角距右邊 30 像素,并且在 VBox 左上角下方 40 像素處。每個組件都有 mouseX 和 mouseY 屬性,它告訴您組件的鼠標指針的相對位置。例如,如果 mouseX 為 -46,mouseY 為 78,那么鼠標指針在組件的坐標系中為向左 46 像素,向下 78 像素。除了局部坐標系之外,Flex 還有一個全局坐標系,該坐標系從整個應用程序的左上角度量 x 和 y 坐標。這些坐標稱為 globalX 和 globalY。
Flex 管理大量更改
在其生命周期中,一個組件經歷一系列的轉換、旋轉、縮放、剪切和拉伸,最后才顯示在屏幕上。每個組件都有一個與之關聯的 matrix
屬性;這個屬性包含關于所有這些調整的信息。因此,x 和 y 坐標、globalX 和 globalY 坐標都通過這些矩陣轉換關聯起來。獲取 x 和 y 值通常需要向 globalX 和 globalY 應用矩陣轉換。從 x 和 y 坐標獲取 globalX 和 globalY 坐標值需要執行反向轉換。Flex 隱藏了所有這些細節,并提供兩個簡化這些轉換過程的方法:localToGlobal()
和 globalToLocal()
。您可以正確地使用這些方法,而不用擔心底層轉換。但是,一定要注意:函數是在點級別上工作的,而不是組件級別。讓我們再看看一個基于 VBox 的例子。清單 8 在 VBox 內部創建了一個標簽:
清單 8. 在 VBox 內部創建標簽
<mx:VBox id="box">
<mx:Label id="myLabel" text="I am a label"/>
</mx:VBox>
box 和 myLabel
都有 localToGlobal()
方法。假設您想要獲得 label
的全局位置。您的第一個想法可能是使用 label
的父容器的 localToGlobal
,因為 label
的 x 和 y 坐標都與它的父容器 box
的 x 和 y 坐標相對應。不過,myLabel
擁有相同的方法,因此您也將調用該方法。結果是這兩種方法都正確,但您需要使用不同的參數調用它們。您可以使用 box.localToGlobal(new Point(myLabel.x,myLabel,y))
或 myLabel.localToGlobal(new Point(0,0))
。當您調用 box.localToGlobal(new Point(myLabel.x,myLabel.y))
時,就是請求 Flex 提供 VBox 中的點的全局位置,其坐標為 myLabel.x
和 myLabel.y
。注意,這與 myLabel
無關。
獲得精確的全局坐標
假設 myLabel
的 x 和 y 坐標分別為 40 和 30。在這個例子中,您要求獲得 VBox
中的一個點 (40,30) 的精確全局位置。在這里,將對 VBox 矩陣進行轉換。myLabel
的坐標系的左上角為 (0,0) —— Flex 組件都是這樣。這意味著在第二個選項中(考慮到 myLabel 的坐標系),您要求 Flex 提供在 myLabel
的坐標系中為 (0,0) 的點的全局位置。不同的方法得到相同的結果,因為它們都從不同的坐標系中選擇相同的點。如果沒有理解這一區別,在本文的后面可能會引起錯誤和困惑。
添加工具提示
使用 Flex 時,有幾個重要的原因要求您獲得全局位置。例如,考慮一個典型的用例,即為組件顯示工具提示。組件提示被添加到 Flex 應用程序的 systemManager
,因此它們位于全局坐標系的內部。不過,您需要為位于其父組件局部坐標系中的組件顯示工具提示。因此,Flex 挑選對應組件的右下角附近的點的局部坐標,然后對其應用 “局部到全局” 的轉換。最后,將繪制一個工具提示組件,并將其添加為組件的子工具提示。工具提示被添加到 Flex 應用程序的 systemManager
,然后放置到將被計算的全局點上。現在考慮另一種情況,在某人單擊控件之后顯示一個菜單,例如瀏覽器左上角的 File 菜單。您不能將這樣的菜單添加為顯示 File 選項的標簽的子元素。例如,如果您將菜單添加為標簽的父元素的子元素,那么 Flex 就將控件垂直放置在該點的下面,因為這是 VBox
的默認行為。如果您將菜單添加為標簽的子元素,那么菜單根本就不會顯示,因為標簽將僅度量它所包含的文本,然后創建一個符合文本的大小。相反,您需要將菜單添加到 systemManager
,它的行為類似于彈出子控件。您可以將添加到 systemManager
的組件放置到整個應用程序區域的任何位置。要將控件準確地放置在標簽的下面,您需要將所需的位置轉換為全局坐標。例如,在標簽的坐標系中標簽的底部點為 (0,label.height)
。然后您可以使用標簽的 localToGlobal()
轉換該點,并將菜單放置到該點上。使用 move()
方法放置菜單,它將任何組件的左上角映射到所選的位置。清單 9 顯示了如何將菜單添加到
標簽控件。
清單 9. 將菜單添加到標簽
<?xml version="1.0" encoding="utf-8"?>
<mx:Label xmlns:mx=http://www.adobe.com/2006/mxml click="showMenu()">

<mx:Script>
<![CDATA[
import mx.controls.Menu;
private function showMenu() : void {
var m : Menu = new Menu();
// populate the menu control with options:
systemManager.addChild(m);
var global : Point = localToGlobal(new
Point(0,height));
m.move(global.x,global.y);
}
]] >
</mx:Script>
</mx:Label>
在這個例子中,單擊標簽將調用一個事件處理程序,這個程序創建一個新的 Menu 控件并填充它(為了保持簡潔,我省略了一些代碼),然后將其作為子元素添加到 systemManager
。這個組件將出現在 systemManager
的 rawChildren 列表中。最后,這段代碼將菜單控件移動到位于標簽控件下面的全局坐標中。注意,轉換是從局部坐標系到全局坐標系,從而支持將控件放置到適當位置。
處理容器時,除了局部和全局坐標系之外,還涉及到另一個坐標系。容器默認創建一個內容面板,用于容納控件的子控件。因此,向容器添加的子控件不是放置在容器的局部坐標系中,而是放置在它的內容面板中,內容面板擁有自己的坐標系。您可能想知道為什么出現內容面板,以及為什么需要內容面板。在容器中,Flex需要能夠添加除了容器之外的更多其他東西,比如滾動條、邊框和覆蓋圖等。這些元素被添加到容器中,而內容面板僅負責處理添加的子元素。如果由于滾動條的原因使子元素不能顯示在容器的可見區域內,那么它仍然存在內容面板中。您需要使用其他方法將該子元素從局部坐標系轉換到內容坐標系。這些方法包括 localToContent()
和 contentToLocal()
。不過,內容坐標系一般用于容器使用子容器的絕對位置的情況。
開始布局階段
就像提交和度量階段一樣,布局階段由調用 invalidateDisplayList() 方法開始。這告知 LayoutManager
當前的顯示列表已經不再有效,應該進行更新。Flex 添加一個組件來將 LayoutManager
傳遞給 invalidatedDisplayListQueue
并在下一次 UI 更新時處理這一指令。LayoutManager
在隊列的每個組件上調用 validateDisplayList()
,而這個方法將調用一個保護方法 updateDisplayList()
。如果您正在創建定制布局,那么需要在調用 super.updateDisplayList()
之后覆蓋該方法,然后替換為希望使用的邏輯。尤其要注意 updateDisplayList()
接受的參數:unscaledWidth
和 unscaledHeight
。這兩個值的命名方式為您放置該組件的內容找到了好位置。這兩個參數共同定義了組件的界限。現在它們的邊界是不限定的。隨后,在這個方法的執行完成之后,Flex 將處理縮放。scaleX
和 scaleY
屬性允許您更改矢量繪制的縮放比例。默認情況下,scaleX
和 scaleY
的值為 1。每個組件都包含一個受保護的 graphics
對象,Flex 使用它來執行矢量繪制。矢量繪制用于完成各種任務,比如填充背景顏色、邊框顏色和漸變等。基于矢量的繪制本身是一個非常寬泛的主題,超出了本文的討論范圍。可以這么說,這個圖形對象公開了 drawCircle()
等方法,您可以使用這些方法繪制基本的圖像和邊框。例如,假設您希望為 Flex Label
控件創建邊框和背景顏色以對其進行擴展。在標準的 Label
控件實現中,這些選項都是不可用的,但清
單 10 顯示了如何擴展它:
清單 10. 擴展 Flex 的 Label
類
package article1.components {
import mx.controls.Label;
public class MyLabel extends Label {
override protected function updateDisplayList(
unscaledWidth:Number, unscaledHeight:Number): void {
super.updateDisplayList(unscaledWidth,unscaledHeight);
graphics.lineStyle(1);
graphics.beginFill(0xcccccc);
graphics.drawRoundRect (0,0,unscaledWidth,unscaledHeight,5,5);
graphics.endFill();
}
這段代碼擴展了 Label
類并覆蓋 updateDisplayList() 方法。在該代碼中,您繪制了一個圓角矩形,將 width
設置為 unscaledWidth,height
設置為 unscaledHeight,并且將實現圓角的參數 ellipseWidth 和 ellipseHeight 設置為 5,5。最后,這段代碼將矩形的背景填充為灰色。您可能注意到該代碼沒有調用 invalidateDisplayList()
。對于擴展 Flex 的 Label
類的類而言,這是不必要的,因為在 Flex 的許多地方都會發生靜默失效。例如,查看清單 11 中的代碼
的作用域,這段代碼來自前一段代碼的 Label
類:
清單 11. 基類中的失效
// some code omitted for clarity
public function set text(value:String): void {
_text = value;
textChanged = true
invalidateProperties();
invalidateSize();
invalidateDisplayList();
}
當標簽的文本設置之后,注意 Flex 如何調用這 3 個失效方法。這意味著您不必顯式地調用它們。這幫助理解為什么在這里調用全部 3 個失效方法。當文本被設置之后,舊的文本就是 “臟的”,并且必須提交新文本 —— 即提交階段。新的文本可能大于或小于舊文本,因此必須重新度量界限。有時,該組件還需要更改布局的某些部分。例如,如果舊的標簽顯示 HTML 格式的文本,而新的文本是簡單的純文本格式,那么就要相應地更改布局。如果文本超出允許的最大大小,那么可能需要刪減標簽和使用工具提示。所有這些問題都是在 updateDisplayList()
階段處理的。此外,當調用 addChild()
時,所有 3 個失效方法都會自動調用。Flex API 深入解釋了失效方法是如何工作的:
“失效方法很少被調用。一般而言,在組件上設置屬性時將自動調用適當的失效方法。”
這里的要點是,大部分用 Flex 編寫的組件在其屬性被更改時將通知適當的失效方法。這要求您對現有的 Flex 控件進行擴展。不過,如果您從頭構建 Flex 控件,那么一定要記得在適當的時間調用失效方法。
序列化
調用這些階段的次序還向您透露一些信息,即什么時候不能調用它們。考慮這樣一個場景,開發人員在擴展 Label 時不小心選擇了在 updateDisplayList()
方法中設置文本屬性。設置文本屬性將調用 validateProperties()
,而后者將調用 commitProperties()
,然后調用 invalidateDisplayList()
,最后調用 validateDisplayList()
和 updateDisplayList()
。最終結果是一個無限遞歸循環,Flash Player 可能會掛起整個應用程序,甚至可能掛起用戶的系統。因此,切忌超越階段邊界。幸運的是,您可以遵循 3 個簡單的規則,從而確保您的組件沒有跨越邊界:
- 除了與大小和位置有關的屬性之外,在提交階段處理所有其他屬性,包括設置樣式、邊框布局、圓角半徑、文本、字體和顏色等。一定不要在這個階段更改可以再次調用 invalidateProperties() 的屬性。
- 在度量階段僅處理與大小相關的屬性。一定不要在這個階段更改可以再次調用 invalidateSize() 的屬性。
- 在這個階段處理與位置、填充、移動、矢量繪制和位圖填充等相關的屬性。一定不要在這個階段更改可以再次調用 invalidateDisplayList() 的屬性。
特別關注 validateNow()
需要特別提到的另一個方法是 validateNow()
。在創建組件時您可能需要立即度量它以獲得它的大小。注意,組件還沒有添加到主顯示列表,因此 LayoutManager
還不能執行度量。考慮這樣一個場景,您需要在畫布的最右上角處放置一個圖像。您先創建一個圖像實例,然后將它移動到右邊。然后執行以下操作:Image.move(canvas.width-image.width,canvas.y);
這個代碼片段不能工作,因為圖像僅被創建,而沒有被度量;所以 image.width
為 0。如果您在調用 image.move()
之前調用 image.validateNow()
,Flex 就會強制調用 LayoutManager
,讓它經歷所有 3 個階段,包括正確度量圖像、為寬度和高度分配有效值的度量階段。幸運的是,Flex 畫布使用了一個基于限制的布局,它允許您為圖像指定限制,比如 'right=0'
。這個指令的基本意思是當在 Flex 畫布上布置圖像時,它將受到這樣的限制:它與畫布右側邊緣之間的距離為 0。您可以通過為圖像指定 right=0
和 top=0
限制實現這種效果。請記住,validateNow()
是一個占用資源很多的方法,除非有充分的理由,否則不要調用它。
從頭構建組件
到目前為止,您已經了解使用 Flex 的好處和一些用例,如何操作和擴展 Flex 組件,并且深入了解了 Flex 組件的生命周期。最后一個步驟是將之前討論的所有呈現特性包含到您從頭構建的定制組件中。我將向您介紹所有必需步驟,展示如何構建一個表示組織的結構的樹圖。注意,這個樣例應用程序使用的是虛假數據。
構建節點
第一步是構建組織樹的一個節點。圖 3 顯示了如何通過添加背景圖片增強節點的外觀。對用于創建圖片的圖片編輯器沒有要求,因此您可以使用自己或您的圖形設計師喜歡的圖片編輯器。
圖 3. 添加背景圖片
調用節點類 Node.as
。清單 12 創建了一個節點元素,并使用圖 3 作為背景圖。
清單 12. 創建節點元素
override protected function updateDisplayList(
unscaledWidth:Number, unscaledHeight:Number): void {
graphics.clear();
var bitmapData : BitmapData = image.content[
"bitmapData"] as BitmapData;
graphics.beginBitmapFill(bitmapData);
graphics.drawRect(0,0,bitmapData.width,bitmapData.height);
graphics.endFill();
graphics.beginFill(0xff0000,.3);
graphics.drawRoundRect(-3,3,bitmapData.width+6,
bitmapData.height+6,10,10);
graphics.drawRoundRect(0,0,bitmapData.width,
bitmapData.height,10,10);
graphics.endFill();
}
graphics.beginBitmapFill()
方法創建了填充節點元素的背景的位圖。圖 4 顯示了 Flex 如何呈現這個組件。
圖 4. Flex 呈現元素節點
清單 13 顯示了如何為組件
添加滾動效果,當用戶滾動到該組件時,它將顯示一個 “焦點” 選擇:
清單 13. 添加滾動效果
public class Node extendsUIComponent {
private var rollOver : Boolean;
public function Node() {
addEventListener(MouseEvent.ROLL_OVER,
function(e : Event) : void {
rollOver = true;
invalidateDisplayList();
});
addEventListener(MouseEvent.ROLL_OUT,
function(e : Event) : void {
rollOver = false;
invalidateDisplayList();
});
}
override protected function
updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number): void {
if(rollOver) {
graphics.beginFill(0x0000ff,.2);
graphics.drawRoundRect(
-10,-10,bitmapData.width+20,
bitmapData.height+20,10,10);
graphics.drawRoundRect(
-3,-3,bitmapData.width+6,
bitmapData.height+6,10,10);
graphics.endFill();
}
}
}
在這段代碼中,您定義了一個布爾變量 rollover 并為 ROLL_OVER
和 ROLL_OUT
添加事件監聽器。在前一種情況中,您將 rollover 設置為 true;在后一種情況中,將 rollover 設置為 false。注意,兩種情況都會導致顯示列表無效,因為您需要 Flex 呈現(或刪除)方框周圍的焦點矩形。
接下來,您需要為應用程序添加一些文本。在這個步驟中,您在 Node 內部定義了一個標簽,并通過覆蓋 createChildren() 方法將該標簽添加為子元素。您在 Node
類和 commitProperties()
方法中定義了一個 nodeName
屬性,然后使用 nodeName
填充標簽的文本字段。接下來,覆蓋 measure()
方法并使用文本行單位度量文本的 height
和 width
。此外,還度量 Node
組件并將其 measuredWidth
和 measuredHeight
設置為包含在內嵌圖片中的 bitmapData
。清單 14 顯示給應用程序添加文本所需的步驟:
清單 14. 給 Flex 應用程序添加文本
private var _nodeName : String;
private function set nodeName(
nodeName : String) : void {
_nodeName = nodeName;
invalidateProperties();
}
override protected function createChildren():void {
text = new Text();
addChild(text);
}
override protected function commitProperties(): void {
super.commitProperties();>
text.text = nodeName;
}
override protected function measure(): void {
super.measure();
var metrics : TextLineMetrics = measureText(nodeName);
text.setActualSize(metrics.width+10,metrics.height+5);
measuredHeight = image.height;
measuredWidth = image.width;
}
注意如何在設置 nodeName 時讓屬性失效。讓屬性失效是為了告知 LayoutManager
屬性發生改變,應該調用提交階段。您還使用 commitProperties()
方法在提交階段設置了標簽的真
正文本。最后,清單 15 顯示了如何向主應用程序文件添加節點元素:
清單 15. 向主文件添加節點元素
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" xmlns:components="article1.components.*">
<components:Node nodeName="John Michael"/>
</mx:Application>
圖 5 顯示了最終的 Flex 控件的節點元素,包含了焦點矩形。
圖 5. 完成后的節點元素

構建鏈接
快要完成了!最后步驟之一是創建一個鏈接來連接節點。構建鏈接比構建節點要簡單得多。清單 16 顯示了如何擴展 UIComponent
、覆蓋 upda
teDisplayList() 并使用鏈接組件的 graphics
對象創建線條。
清單 16. 創建鏈接
package article1.components {
import mx.core.UIComponent;
public class Link extends UIComponent {
public var fromNode : Node;
public var toNode : Node;
override protected function updateDisplayList
(unscaledWidth:Number,
unscaledHeight:Number): void {
graphics.clear();
graphics.lineStyle(4,0x0000ff,.5);
graphics.moveTo(fromNode.x+fromNode.width/2,
fromNode.y+fromNode.height/2);
graphics.lineTo(toNode.x+toNode.width/2,
toNode.y+toNode.height/2);
}
}
}
您創建了一個純粹基于矢量的繪圖,并且顯示了一
條從源節點到目標節點的連線。清單 17 顯示了如何將節點連接在一起,并創建一個樹圖。
清單 17. 合并所有部分
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" xmlns:components="article1.components.* ">
<components:Link fromNode="{node1}" toNode="{node2}"/>
<components:Link fromNode="{node2}" toNode="{node3}"/>
<components:Link fromNode="{node1}" toNode="{node3}"/>
<components:Node id="node1" nodeName="John Michael" x="400" y="300"/>
<components:Node id="node2" nodeName="Harris Scott" x="700" y="600"/>
<components:Node id="node3" nodeName="Paul Simpson" x="100" y="600"/>
</mx:Application>
圖 6 顯示了最終的定制 Flex 組織樹圖應用程序。
圖 6. 最終的 UI
您可以從小節獲取本文描述的定制 Flex 組件的完整源代碼。
至此,您已經創建了一個包含 3 個節點的樹圖組件。添加更多的節點和鏈接非常簡單,并且與 Flex 呈現引擎無關,因此留給您處理。您可以使用這種類型的組件表示組織樹圖關系、路由鏈接拓撲和類似的應用程序。您甚至不需要創建這種類型的應用程序,您可以使用本文討論的技術和樣例構建可擴展的定制 Flex 組件,以滿足特定的業務需求。