原文: http://www.adobe.com/devnet/flex/articles/flex4_skinning.html
[原創翻譯鏈接: http://www.smithfox.com/?e=34 轉載請保留此聲明]
Flex 4(代號:Gumbo)的主要主題之一是"Design in mind", 皮膚則是這個主題的重要組成部分。Flash Player是迄今爲止最具創意的web工作機制。然而,Flex應用程序卻有了一個不太好的名聲:大部分程序看上去都很相似,那是因爲許多開發人員選擇了Flex默認的外觀和體驗(比如Halo)而不是應用豐富的樣式和皮膚。
Flex 4中可以更容易地完全改變應用程序的外觀和體驗。新的皮膚架構建立在Flex 4的一些架構改動以及邏輯和組件視覺元素的清晰分離的基礎上。正因爲如此,在Flex 4的組件沒有直接包含任何有關他們的視覺外觀的信息。所有這些信息包含在皮膚文件中,這要感謝FXG和新的states語法,使新的皮膚文件完全可以用MXML編寫,這樣它們就更容易地被閱讀和編寫,同時也更易於工具訪問。
在這篇文章中,您將瞭解在Flex 4對皮膚架構的改進。 通過編寫一個按鈕的基本皮膚,你會學到一點關於FXG和新states的語法。 接下來,您將通過製作一個slider皮膚的過程瞭解怎麼用契約在組件和皮膚進行交互。 最後,您將通過創建一個新組件的皮膚來深入學習可變換皮膚的組件。
注:在本文檔中,Halo組件是指Flex 3已經有的組件。 Spark組件指的是在Flex 4中一套新組件。
編寫一個簡單的按鈕皮膚
FXG是利用Flash Player作矢量圖形的聲明標記語言。 用他你可以很容易地創建一個自定義按鈕。 這個按鈕,只是簡單地在一個矩形框裏面放些文字(見圖1)。
Sample1.mxml
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"> <s:Group verticalCenter="0" horizontalCenter="0"> <s:Rect id="rect" radiusX="4" radiusY="4" top="0" right="0" bottom="0" left="0"> <s:fill> <s:SolidColor color="0x77CC22" /> </s:fill> <s:stroke> <s:SolidColorStroke color="0x131313" weight="2"/> </s:stroke> </s:Rect> <s:Label text="Button!" color="0x131313" textAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="1" left="12" right="12" top="6" bottom="6" /> </s:Group> </s:Application>
圖1: sample1 按鈕
如果你熟悉Flex 3,你一定熟悉上面的語法,雖然你可能不熟悉所使用的有些特別的組件。 Goup容器是Spark中基本的沒有樣式的容器。 Rect是一個新的FXG圖元,沒錯,一個矩形。 在文檔中的最後標籤Lable是Spark中的新的文本組件。 讀MXML時就像在描述一個組件,它是一個用1像素深灰色畫出圓角的長方形,中間是一些綠色的文字。
FXG好處之一是,它不僅是讓我們更容易理解繪畫語句,而且因爲FXG使用XML結構所以使得他可以用工具創作。 如需有關FXG信息,請參閱FXG規範 。
轉換你的按鈕圖形爲一個按鈕皮膚
到目前爲止,MXML文件還只是一個不能交互的靜態的作品。 它還沒采取Flex 4新的皮膚功能。 爲此,你需要把它應用到Button組件並使用它作爲一個皮膚。 要創建Spark皮膚文件,用Skin作爲新的MXML文件的根標籤。 然後,將上面的圖形代碼copy過來:
ButtonSkin1.mxml
<?xml version="1.0" encoding="utf-8"?> <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" alpha.disabled=".5"> <!-- states --> <s:states> <s:State name="up" /> <s:State name="over" /> <s:State name="down" /> <s:State name="disabled" /> </s:states> <!-- border and fill --> <s:Rect id="rect" radiusX="4" radiusY="4" top="0" right="0" bottom="0" left="0"> <s:fill> <s:SolidColor color="0x77CC22" /> </s:fill> <s:stroke> <s:SolidColorStroke color="0x131313" weight="2"/> </s:stroke> </s:Rect> <!-- text --> <s:Label text="Button!" color="0x131313" textAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="1" left="12" right="12" top="6" bottom="6" /> </s:Skin>
你會發現還多了一些states。 我將稍後討論這個。
皮膚文件完成後,你需要將它關聯到一個按鈕組件。 Spark架構中,每一個可變換皮膚組件是通過skinClass CSS樣式來和皮膚關聯起來,這個CSS樣式可以用樣式表設置或者直接寫在MXML內。 當前例子中,稍後再使用:
Sample2.mxml
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"> <s:Button verticalCenter="0" horizontalCenter="0" skinClass="ButtonSkin1" click="trace('I\'ve been clicked!')" focusIn="trace('focus...on me?')" /> </s:Application>
圖2: sample2 按鈕
現在,您已經將一個新的皮膚文件應用到這個按鈕了。 按鈕組件包含所有按鈕的行爲邏輯。 它添加事件監聽器,發送新的事件,指示組件所處state,等等。 皮膚無需處理組件中定義的所有可視元素。
但是,這個按鈕現在看起來和之前創建的靜態圖形沒有什麼不同。 按鈕應該是互動的,但還不是這樣。這是因爲你還沒有定義在不同states下的按鈕外觀。
介紹皮膚契約(contract)
一個靜態的皮膚很無聊。 爲了有點趣,皮膚必須能與組件交互,反之亦然。 這兩個因素通過皮膚契約進行交互。 有三個要點是:皮膚states,data和parts(見圖3)。 一方面,組件定義了這三種不同要點,另一方面,皮膚則處理這三個要點。
圖3: 皮膚契約包含 data, parts, 和 states.
定義皮膚states
在Spark中的每個可變換皮膚組件都有一組皮膚states。 你可以依據組件所處皮膚state來改變你的皮膚外觀。對於一個按鈕有四種基本皮膚states: up,over,down和disabled。 您可以爲這些皮膚狀態定義不同的外觀(見圖4)。
ButtonSkin2.mxml
<?xml version="1.0" encoding="utf-8"?> <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" alpha.disabled=".5"> <!-- states --> <s:states> <s:State name="up" /> <s:State name="over" /> <s:State name="down" /> <s:State name="disabled" /> </s:states> <!-- dropshadow for the down state only --> <s:Rect radiusX="4" radiusY="4" top="0" right="0" bottom="0" left="0" includeIn="down"> <s:fill> <s:SolidColor color="0"/> </s:fill> <s:filters> <s:DropShadowFilter knockout="true" blurX="5" blurY="5" alpha="0.32" distance="2" /> </s:filters> </s:Rect> <!-- border and fill --> <s:Rect id="rect" radiusX="4" radiusY="4" top="0" right="0" bottom="0" left="0"> <s:fill> <s:SolidColor color="0x77CC22" color.over="0x92D64E" color.down="0x67A41D"/> </s:fill> <s:stroke> <s:SolidColorStroke color="0x131313" weight="2"/> </s:stroke> </s:Rect> <!-- highlight on top --> <s:Rect radiusX="4" radiusY="4" top="2" right="2" left="2" height="50%"> <s:fill> <s:LinearGradient rotation="90"> <s:GradientEntry color="0xFFFFFF" alpha=".5"/> <s:GradientEntry color="0xFFFFFF" alpha=".1"/> </s:LinearGradient> </s:fill> </s:Rect> <!-- text --> <s:Label text="Button!" color="0x131313" textAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="1" left="12" right="12" top="6" bottom="6" /> </s:Skin>
ButtonSkin2.mxml<h4>ButtonSkin2.mxml</h4>
圖4. 按鈕的四個皮膚states
不同皮膚狀態,組件看起來不同是因爲你皮膚定義的不同。 這個皮膚文件採用了新的states語法。 這是Flex 4新功能,這使得編寫state更加清晰和簡潔。 語法是property.stateName="property所處狀態的值值"。 例如, alpha.disabled=".5"是指當按鈕進入disabled皮膚state,皮膚會改變alpha爲50%。over和down狀態,我定義了不同的填充色,color.over="0x92D64E" color.down="0x67A41D"。
新的state語法爲MXML組件增加了includeIn和excludeFrom屬性。 按鈕陰影皮膚僅包含在down state,這使按鈕按下時很好看。此外,爲了更加地生動,所有states下我都添加了另一個矩形使按鈕頂部突出。
注:更多Flex 4中語法增強信息,請查看 新的states語法規範。
基於皮膚state而改變按鈕外觀,使得操作按鈕有一種交互的體驗。但你會發現,該按鈕組件的文本是硬編碼爲"Button!"。 在下一節中,你將看到皮膚如何顯示組件的數據,當前例子中,數據就是label屬性。
從組件獲取數據
我建議你總是把HostComponent元數據聲明在你的皮膚。HostComponent元數據指向你皮膚的組件,通過它可以在皮膚中訪問組件。在按鈕皮膚,你可以使用這個hostComponent屬性綁定到按鈕label屬性。
ButtonSkin3.mxml:
<?xml version="1.0" encoding="utf-8"?> <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" alpha.disabled=".5"> <fx:Metadata> [HostComponent("spark.components.Button")] </fx:Metadata> ... <!-- text --> <s:Label text="{hostComponent.label}" color="0x131313" textAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="1" left="12" right="12" top="6" bottom="6" /> </s:Skin>
當聲明按鈕後, 皮膚中的文字會顯示label屬性值.
Sample4.mxml:
<?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark"> <fx:Style> @namespace s "library://ns.adobe.com/flex/spark"; s|Button { skinClass: ClassReference("ButtonSkin3"); } </fx:Style> <s:layout> <s:VerticalLayout /> </s:layout> <s:Button label="Button #1" /> <s:Button label="Button #2" /> <s:Button label="Button #3" /> </s:Application>
主應用程序聲明瞭三個按鈕。 每個按鈕都使用相同的皮膚文件ButtonSkin3,這是由CSS類型選擇器定義的。但是,每個按鈕都有不同的標籤。 因爲現在的皮膚拉(pulls)組件的label屬性來顯示文本,按鈕看起來像你期盼的那樣,顯示不同的文字(見圖5)。
圖5: 按鈕顯示他自己的文字
你已經看到了皮膚契約三個要點中的兩個,states和data。 皮膚state是一種組件進行交互的方式,而皮膚則定義了這些states的外觀和體驗。數據,這些用戶可設置的組件屬性,通過使用HostComponent元數據和hostComponent屬性能被拉到(原文:can be pulled into)皮膚中。 在上面的例子,皮膚從按鈕組件中拉數據(按鈕組件的label屬性)。另一種方法是用皮膚parts(第三個要點)將數據推(push)到皮膚中。
繼續談皮膚契約: 皮膚parts
皮膚parts是皮膚契約的第三部分。在Spark中,每個可變換皮膚的組件都有一組皮膚parts用來幫助定義組件。 以scrollbar爲例,有四個皮膚parts:增加按鈕,減少按鈕,軌跡帶和滾動條。 再以按鈕爲例,他僅有一個皮膚parts,labelDisplay。這是按鈕組件要求提供的一部分。在上面的按鈕皮膚例子中,與其綁定文本爲{hostComponent.label} ,還不如你提供一個文本組件的id labelDisplay,按鈕會識別這個皮膚part,從面將label屬性推送到皮膚中。
ButtonSkin4.mxml:
<?xml version="1.0" encoding="utf-8"?> <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" alpha.disabled=".5"> <fx:Metadata> [HostComponent("spark.components.Button")] </fx:Metadata> <!-- states --> <s:states> <s:State name="up" /> <s:State name="over" /> <s:State name="down" /> <s:State name="disabled" /> </s:states> <!-- dropshadow for the down state only --> <s:Rect radiusX="4" radiusY="4" top="0" right="0" bottom="0" left="0" includeIn="down"> <s:fill> <s:SolidColor color="0"/> </s:fill> <s:filters> <s:DropShadowFilter knockout="true" blurX="5" blurY="5" alpha="0.32" distance="2" /> </s:filters> </s:Rect> <!-- border and fill --> <s:Rect id="rect" radiusX="4" radiusY="4" top="0" right="0" bottom="0" left="0"> <s:fill> <s:SolidColor color="0x77CC22" color.over="0x92D64E" color.down="0x67A41D"/> </s:fill> <s:stroke> <s:SolidColorStroke color="0x131313" weight="2"/> </s:stroke> </s:Rect> <!-- highlight on top --> <s:Rect radiusX="4" radiusY="4" top="2" right="2" left="2" height="50%"> <s:fill> <s:LinearGradient rotation="90"> <s:GradientEntry color="0xFFFFFF" alpha=".5"/> <s:GradientEntry color="0xFFFFFF" alpha=".1"/> </s:LinearGradient> </s:fill> </s:Rect> <!-- text --> <s:Label id="labelDisplay" color="0x131313" textAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="1" left="12" right="12" top="6" bottom="6" /> <!-- transitions --> <s:transitions> <s:Transition> <s:CrossFade target="{rect}" /> </s:Transition> </s:transitions> </s:Skin>
Label不再綁定到hostComponent。相反,我給它一個id labelDisplay ,這是一個按鈕組件所需的皮膚part。按鈕組件自動處理數據,將它的label屬性推送到labelDisplay。
除了分配一個label皮膚part,我還在皮膚中添加了一個簡單的CrossFade transition。皮膚文件是定義組件的所有可視化方面的地方,包括transition。當前例子中,隨時改變按鈕狀態,你會看到不同state之間切換時的漸變效果。
製作slider皮膚
皮膚parts不僅可以推送組件數據到皮膚中,組件也可以用它們來註冊行爲。爲了講得更明白,以slider組件爲例。slider兩個主要parts是軌跡條和滑動塊。在這個例子中,該組件沒有把任何數據推送到皮膚來顯示,但它添加了事件監聽器到parts中並且會根據組件的value屬性執行滑動塊的佈局。例如,當點擊軌跡條,組件會更新其value屬性並且定位滑動塊到適當位置。此外,還有動態皮膚parts,數據提示,這是用來拖動滑塊時顯示彈出的提示信息。 在圖6所示是一個簡單slider和一個修改後的slider。
圖6: 修改後的slider(左邊) 和原來的slider (右邊)
爲構建這個, 你的皮膚文件必須聲明三個皮膚parts: thumb(滑塊), track(軌跡條), and dataTip(數據提示)。
MySliderSkin.mxml
<?xml version="1.0" encoding="utf-8"?> <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" minWidth="11" minHeight="100" alpha.disabled="0.5"> <fx:Metadata> [HostComponent("spark.components.VSlider")] </fx:Metadata> <s:states> <s:State name="normal" /> <s:State name="disabled" /> </s:states> <fx:Declarations> <fx:Component id="dataTip"> <s:DataRenderer minHeight="24" minWidth="40" x="20"> <s:Rect top="0" left="0" right="0" bottom="0"> <s:fill> <s:SolidColor color="0xFFF46B" alpha=".9"/> </s:fill> <s:filters> <s:DropShadowFilter angle="90" color="0x999999" distance="3"/> </s:filters> </s:Rect> <s:Label id="labelField" text="{data}" horizontalCenter="0" verticalCenter="1" left="5" right="5" top="5" bottom="5" textAlign="center" verticalAlign="middle" color="0x555555" /> </s:DataRenderer> </fx:Component> </fx:Declarations> <s:Button id="track" left="5" right="5" top="0" bottom="0" skinClass="MyTrackSkin" /> <s:Button id="thumb" left="0" right="0" width="18" height="8" skinClass="MyThumbSkin" /> </s:Skin>
在皮膚中定義了這些皮膚parts這後,組件負責處理他們。它添加事件監聽器到滑塊,讓你可以在軌跡條中拖動滑塊。 它還根據相應的value值來定位滑塊。請看一下上面示例代碼中的MyTrackSkin和MyThumbSkin。你會看到許多FXG的例子。請注意,自定義的滑塊皮膚相比默認Spark滑塊皮膚有着完全不同的形狀。
"數據提示"皮膚part是動態的 -- 它負責生成和佈局。當前的例子中,當你拖動滑塊,會在滑塊右邊彈出數據提示。有了皮膚契約,皮膚可以只管定義皮膚part,和所有可視化方面的內容,而不必擔心有什麼副作用。 所有的銜接都由組件來處理。
注:一些Flex 4內置組件不僅附加行爲到皮膚parts上,同時也會推送數據到皮膚parts。另一種得到皮膚中數據的方法是通過hostComponent屬性來拉他們。
當一個組件創建了皮膚時,並非所有的皮膚parts是必需的。 例如,VSlider的數據提示皮膚part並不是必需的。 如果它不存在,就不顯示數據提示。
創建可變換皮膚組件
Spark可變換皮膚組件沒有在幕後做什麼特別的事情。. They have data properties and advertise the skin parts and skin states they need through metadata。 他們還有少許關鍵的方法來用管理皮膚和皮膚parts的生命週期。您也可以和它一樣輕鬆地創建一個新的換膚組件。
爲了演示一下,你可以創建一個簡單NoteCard組件,它可以用來在屏幕上顯示筆記。在圖7所示的例子中,應用程序隨機創建多個語錄。
圖7: NoteCard 組件例子
主應用程序僅創建一個有點旋轉的語錄NoteCard。 有趣的部分是NoteCard類,它擴展了spark.components.supportClasses.SkinnableComponent類並且在生命週期方法中添加代碼。
NoteCard.as:
package { [SkinState("normal")] [SkinState("disabled")] public class NoteCard extends SkinnableComponent { public function NoteCard() { super(); } [SkinPart(required="true")] public var labelDisplay:TextBase; [SkinPart(required="false")] public var closeButton:Button; private var _text:String; public function get text():String { return _text; } public function set text(value:String):void { if (_text == value) return; _text = value; } ... } }
此組件聲明數據屬性,皮膚states,皮膚parts。 對於數據,NoteCard有一個公共的text屬性。 此外,NoteCard有兩個皮膚states,normal和disabled,用SkinStates元數據聲明在類聲明代碼的上面。 這就告訴皮膚,它需要實現這兩個states。
NoteCard還有兩個皮膚parts,是通過SkinPart元數據聲明的。 SkinPart元數就直接聲明在皮膚part名稱之上。 當前例子,labelDisplay是必需的TextBase類皮膚part,closeButton是一個可選的Button類皮膚part。
由於皮膚是運行時載入,當組件第一次啓動時,你不能保證有一定有皮膚。 你也不能保證已經有了全部的皮膚parts,尤其是他們是可選的。 框架負責了這些事情: 銜接part聲明和組件屬性定義,並且通過皮膚生命週期方法通知組件parts已經準備好了。
實現皮膚states
爲實現皮膚states,你需重寫getCurrentSkinState()方法以返回皮膚當前所處狀態,當前例子中,它會返回"normal"或"disabled"。當一些事件導致皮膚state變得無效時,組件應該調用invalidateSkinState()方法。NoteCard.as
package { [SkinState("normal")] [SkinState("disabled")] public class NoteCard extends SkinnableComponent { ... override public function set enabled(value:Boolean) : void { if (enabled != value) invalidateSkinState(); super.enabled = value; } override protected function getCurrentSkinState() : String { if (!enabled) return "disabled"; return "normal" } ... } }
當設置enabled屬性時,enabled setter調用invalidateSkinState()以通知皮膚,組件的state需要改變,這樣getCurrentSkinState()隨即會被調用。
處理皮膚parts
處理皮膚parts,有兩種主要方法應該要重寫,partAdded()和partRemoved()。這些方法會告訴你一個特定的皮膚part被添加了或被刪除了。當裝載一個皮膚時Parts將被加入或是刪除。皮膚是在運行時交換的,並且延遲加載的,所以只有在某種states情況下或者是一個動態part剛剛被創建時part纔會被加入。在partAdded()方法你可以設置你想要的任何數據到part,而且也可以attach一些事件偵聽到part上。當part被刪除時,你應該在partRemoved()方法中做相反的事情。
NoteCard.as
package { public class NoteCard extends SkinnableComponent { [SkinPart(required="true")] public var labelDisplay:TextBase; [SkinPart(required="false")] public var closeButton:Button; public function set text(value:String):void { if (_text == value) return; _text = value; if (labelDisplay) labelDisplay.text = value; } override protected function partAdded(partName:String, instance:Object) : void { super.partAdded(partName, instance); if (instance == labelDisplay) labelDisplay.text = _text; if (instance == closeButton) closeButton.addEventListener(MouseEvent.CLICK, closeButton_clickHandler); } override protected function partRemoved(partName:String, instance:Object) : void { super.partRemoved(partName, instance); if (instance == closeButton) closeButton.removeEventListener(MouseEvent.CLICK, closeButton_clickHandler); } protected function closeButton_clickHandler(event:MouseEvent) : void { event.stopPropagation(); IVisualElementContainer(parent).removeElement(this); } } }
在partAdded()方法中,當labelDisplay part加入時,我設置text到這個part。此外,在text屬性的setter方法中,我檢查,看看是否已經加入labelDisplay,如果是的話,我重新設置labelDisplay.text爲組件的_text值以保證和組件text屬性同步。在partAdded()方法中,我添加一個click事件監聽器到closeButton皮膚part。在partRemoved()我一定要刪除這個click事件監聽器。
作爲一個SkinnableComponent ,你需要做的就是利用這個強大的換膚機制。 當有人創造了某個組件的皮膚,他們必須實現皮膚states和皮膚parts以得到期望的組件行爲。 在圖6所示的皮膚在樣例源代碼中可以找到,即使這是一個簡單的組件定義,你依然可以用不同的皮膚完全改變它的外觀和體驗。這就是皮膚真正的力量。
注:當創建可變換皮膚組件,您可能要決定某些行爲是屬於皮膚的還是組件。 沒有一個明確的硬性的規則。只要能讓你的工作更容易就行了。 作爲一般指導,一切外觀和感觀的定義應在皮膚MXML文件中聲明。 另一方面,如果有多個皮膚想要某個特殊行爲,那麼將這個行爲放在組件可能是一個好主意。例如,slider中滑塊的定位是做在VSlider和HSlider,沒有在皮膚上。
下一步到哪裏
Flex 4皮膚髮生了重大修改。 明確分開了組件和皮膚。該組件包含了數據,行爲和核心邏輯,而皮膚定義了組件的外觀和體驗。組件由ActionScript編寫而皮膚寫在MXML中,這是託FXG和新states語法的福。 組件和皮膚通過皮膚契約進行交互。 又因爲它們是各自獨立的文件,所以新的皮膚很容易應用到組件上從而完全改變他們的外觀。
欲瞭解更多的Flex 4皮膚信息,請查看 皮膚架構規範 以及 Gumbo組件架構白皮書。