1.概述 本文介绍了Gumbo的皮肤框架原理。不象MX的skinning仅是一个简单的图形操作,Gumbo的skin是一个综合的、可以包含多个元素(比如图形元素,文本、图片、转换等),主要亮点如下:
. 所有可视特征,包括layou都可以在skin中控制。
. 所有的skin保持简单、一致,Button的皮肤与List Item的皮肤可以是一样的
. 通过skin parts可以进行皮肤的组合
. 通过配置文件描述可以容易创建和控制
2.使用场景最常用场景:
有一个控件,比如Button,不通过任何ActionScript代码,只用创建一个新的ButtonSkin ,因为skin 文件是MXML,设计人员很容易进行设计皮肤。
其它场景:
开发人员创建一个自定义组件,但可以自定义皮肤。通过继承SkinnableComponent即可。
设计人员通过修改CSS就可以改变应用程序的皮肤。
3.详细描述 每个组件包含四个概念:
model, skin, controller和view-specific logic(可视化描述逻辑)。
model 包含组件的属性和业务逻辑。比如Range model的属性有:minimumValue, maximumValue, stepSize等。方法有:画线。model 不能包含任何可视化或行为信息。
skin 定义了组件的可视化元素。
controller 是skin和model的接口。它具有几个关键功能:
. 定义组件行为,比如:Button控制器具有mouse事件处理逻辑。
. 定义组件的可视化状态
. 定义组件的组成部分。比如一个scrollbar控制器具有4部分:up arrow, down arrow, thumb, 和track。
view-specific logic描述skin中不同组件的位置和大小,比如HScrollBar 和 VScrollBar具有不同的可视描述逻辑,决定thumb的位置。为了通知scrollbar,必须重载其逻辑。
skinnable componen在编程和运行期间,其皮肤都可以改变。
4.组装 . model 和 controller可以用ActionScript编写。
. model和controller有时并不容易区分,尤其当model是UI组件时。
. skin以MXML格式编写
. view-specific logic可以写在skin文件中,但是绝大多数时候,logic应该放到组件中去,或者是组件的一个子类,比如ScrollBarBase->HScrollBar 或 ScrollBarBase->VScrollBar.
. 对于skinnable组件, 通过 CSS与skins关联。
其关系如下
左边的绿箭头表示继承关系,红箭头表示CSS转换。
5.在skins中编码 VS 在组件中编码 通常,应该把所有的可视逻辑放在skin中,尤其是想做成可定制的。但是,这很难清楚的划分。
view-specific logic 可以放在skin中,也可以放在组件中。
一般来讲,在多个skin中使用的代码应放在组件中, 特定的皮肤描述应放在skin中。
比如,scrollbar拥有position和size属性,因为对于所有的scrollbars 都需要这二个属性。因为多个skin利用view-specific logic去定位scrollbar,因此就有了HScrollBar 子类来完成这个需求。
如果开发人员只是创建了一次性使用的组件,可以把view-specific logic放在skin文件中,而不是组件的子类中。
6.状态 一些组件,象Button,可以在skin中使用状态。组件通过SkinState 元数据定义这些状态,并在skin中定义这些状态。
在组件中
- /**
-
* Up State of the Button
-
*/
-
[SkinState("up")]
-
-
/**
-
* Over State of the Button
-
*/
-
[SkinState("over")]
-
-
/**
-
* Down State of the Button
-
*/
-
[SkinState("down")]
-
-
/**
-
* Disabled State of the Button
-
*/
-
[SkinState("disabled")]
-
-
public class Button extends SkinnableComponent {
-
...
-
}
在skin中
- <s:Skin xmlns:fx="" xmlns:s="library://ns.adobe.com/flex/spark">
-
<s:states>
-
<s:State name="up" />
-
<s:State name="over" />
-
<s:State name="down" />
-
<s:State name="disabled" />
-
</s:states>
-
-
...
-
</s:Skin>
在组件中的controller 代码决定skin的状态,通过skin中的currentState属性定义当前状态。
SkinState 元数据是一种组件在skin中定义状态的方式。如果skin没有定义需要的状态,编译器将抛出错误。
子类可以从父类中继承SkinState,比如:
Button声明skin states : "up", "over", "down", "disabled"。
ToggleButton 声明skin states : "upAndSelected", "overAndSelected", "downAndSelected", "disabledAndSelected" ,并继承了Button所有的skin状态。
一个skin的状态不必与组件的状态相同。button的currentState 不必与ButtonSkin的currentState 属性一致。button 影响skin的状态。用户通过设置组件的属性影响skin的状态,比如设置toggleButton selected=true/false。
开发人员可以使用invalidateSkinState() 和getCurrentSkinState() 改变skin的状态, invalidateSkinState() 验证skin状态。
例如 (Button.as):
- package spark.components
-
{
-
-
/**
-
* Up State of the Button
-
*/
-
[SkinState("up")]
-
-
/**
-
* Over State of the Button
-
*/
-
[SkinState("over")]
-
-
/**
-
* Down State of the Button
-
*/
-
[SkinState("down")]
-
-
/**
-
* Disabled State of the Button
-
*/
-
[SkinState("disabled")]
-
-
public class Button extends SkinnableComponent implements IFocusManagerComponent
-
{
-
-
...
-
-
/**
-
* @return Returns true when the mouse cursor is over the button.
-
*/
-
public function get isHoveredOver():Boolean
-
{
-
return flags.isSet(isHoveredOverFlag);
-
}
-
-
/**
-
* Sets the flag indicating whether the mouse cursor
-
* is over the button.
-
*/
-
protected function setHoveredOver(value:Boolean):void
-
{
-
if (!flags.update(isHoveredOverFlag, value))
-
return;
-
-
invalidateSkinState();
-
}
-
-
...
-
-
// GetState returns a string representation of the component's state as
-
// a combination of some of its public properties
-
protected override function getUpdatedSkinState():String
-
{
-
if (!isEnabled)
-
return "disabled";
-
-
if (isDown())
-
return "down";
-
-
if (isHoveredOver || isMouseCaptured )
-
return "over";
-
-
return "up";
-
}
-
-
...
-
-
//--------------------------------------------------------------------------
-
//
-
// Event handling
-
//
-
//--------------------------------------------------------------------------
-
-
protected function mouseEventHandler(event:Event):void
-
{
-
var mouseEvent:MouseEvent = event as MouseEvent;
-
switch (event.type)
-
{
-
case MouseEvent.ROLL_OVER:
-
{
-
// if the user rolls over while holding the mouse button
-
if (mouseEvent.buttonDown && !isMouseCaptured)
-
return;
-
setHoveredOver(true);
-
break;
-
}
-
-
case MouseEvent.ROLL_OUT:
-
{
-
setHoveredOver(false);
-
break;
-
}
-
-
...
-
}
-
}
-
}
-
-
}
7.数据交互 skin通过[HostComponent]元标签获取组件的引用,定义了元标签后,我们可以在编译期检查skin,以确保所有skin parts和skin state被显示。
有两种方式使得Data和组件之间交互。一是推方法;二是拉方法。推方法意味着从组件中获取属性,并设置到skin的适当位置上。另一个方法是skin请示hostcomponent的属性值。
拉方法是在skin和component之间使用binding。推方法是组件将值传递给skin。比如Button有个label的setter方法。我们推这个值到labelDisplay skin part。
8.Component/Skin Parts除了skin state,Component/Skin的交互是part。
一些组件由独立的part构成,一个NumericStepper包含一个up button,donw button和text。
有两个类型的part:static和dynamic。Static parts由skin自动实例化,static part仅有一个实例。Dynamic part需要时实例化,存在多个实例,以scrollbar为例,有四个皮肤parts:增加按钮,减少按钮,轨迹带和滚动条。
定义组件上的skin part
[SkinPart]用于声明part。元标签可用于任何public属性。属性的名字变成了part的一部分。这有个例子,skin part定义了"thumb",作为Button的属性。
[SkinPart(required="true")]
public var thumb:Button;
[SkinPart]元标签的静态part有一个可选属性:required。如果设置required="true",必须确保该part为非null。
这有个非必须skin part的例子
[SkinPart(required="false")]
public var upButton:Button;
当一个组件需要一个或更多的part实例。比如Accordion 需要创建多个导航头。动态part可以在[SkinPart]中定义,但是变量类型必须是IFactory。
这里有一个例子,工厂创建spark.components.Button类型的实例。Dynamic parts也是可选的。
[SkinPart(type="spark.components.Button")]
public var headerFactory:IFactory;
9.定义skin中的skin part下面是一个包含thumb part的例子
- <s:Skin xmlns:fx="" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:local="*">
- <!-- other stuff here... -->
- <!-- "thumb" 在此定义. MyCustomThumb"类的实例,Button的子类, 属性id用于绑定组件part元素 -->
- <local:MyCustomThumb id="thumb" ... />
- <!-- other stuff here... -->
- </s:Skin>
skin part可以在skin文件中任何地方定义。如果没有定义,在组件皮肤渲染时将抛出运行时错误。如果HostComponent 元标签在skin中定义了,将抛出编译错误。
动态part,应该在skin文件中的Declarations 部分。Declarations 是MXML的一部分,下面是一个skin包含动态par "theaderFactory"的例子。
- <s:Skin xmlns:fx="" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:local="*">
- <fx:Declarations>
- <fx:Component id="headerFactory">
- <local:MyCustomHeaderControl />
- </fx:Component >
- <!-- More dynamic part definitions can go here... -->
- </fx:Declarations>
- <!-- Static skin elements and parts go here... -->
- </s:Skin>
10.static part运行 当加载一个包含static part的skin,SkinnableComponent 基类将将所有皮肤中的static part分配给组件声明的part变量上。一旦分配完成,组件将仅直接引用实例变量。
当变量被分配后,partAdded()函数被调用。在skin被卸载时,partRemoved()函数被调用。
下面是一个简单的例子。
- override protected function partAdded(partName:String, instance:*):void
- {
- super.partAdded(partName, instance);
-
- // For static parts we can just compare the instance with the property
- if (instance == thumb)
- thumb.addEventListener(...);
- }
- override protected function partRemoved(partName:String, instance:*):void
- {
- super.partRemoved(partName, instance);
-
- if (instance == thumb)
- thumb.removeEventListener(...);
- }
11.dynamic part运行 使用createDynamicPartInstance()创建的动态part实例,使用removeDynamicPartInstance()移除。当一个动态part被add/remove,方法partAdded()和 partRemoved()被调用。
下面是一个使用动态part "separator"的例子
- public class MyComponent extends SkinnableComponent
- {
- /**
- * Declare the "separator" part
- */
- [SkinPart(type="flash.display.DisplayObject")]
- public var separator:IFactory;
- protected const SEPARATOR_NAME:String = "separator";
- ...
- protected function createSeparator():void
- {
- var separator:DisplayObject = createPartInstance(SEPARATOR_NAME);
-
- separator.alpha = Math.random();
- separators[i] = separator;
- skin.addElement(separator);
- }
-
- override protected function partAdded(partName:String, instance:*):void
- {
- super.partAdded(partName, instance);
- if (partName == SEPARATOR_NAME)
- {
- // A new separator has been created.
-
- // Add event listeners
- instance.addEventListener(...);
- }
- }
- override protected function partRemoved(partName:String, instance:*):void
- {
- super.partRemoved(partName, instance);
- if (partName == SEPARATOR_NAME)
- {
- // A separator is being removed
- // Remove event listeners
- instance.removeEventListener(...);
- }
- }
- }
- }
createSeparator()根据skin的dynamic part创建了一个separator ,并设置了一个随机的alpha值。createSeparator()调用了createDynamicPartInstance(),这将自动调用partAdded()。
但是,如果你想在新的skin中创建dynamic part,一旦skin加载,你必须再一次创建他们。不能自动创建的原因是每个动态part都有状态。
如果你想在最新加载的skin中做些事情,则调用super.attachSkin()。卸载skin则调用detachSkin()。
- public class MyComponent extends SkinnableComponent
- {
- /**
- * Declare the "separator" part
- */
- [SkinPart(type="flash.display.DisplayObject")]
- public var separator:IFactory;
- protected const SEPARATOR_NAME:String = "separator";
- ...
- protected function createSeparator():void
- {
- var separator:DisplayObject = createPartInstance(SEPARATOR_NAME);
-
- separator.alpha = Math.random();
- separators.push(separator);
- skin.addElement(separator);
- }
-
- override protected function partAdded(partName:String, instance:*):void
- {
- super.partAdded(partName, instance);
- if (partName == SEPARATOR_NAME)
- {
- // A new separator has been created.
-
- // Add event listeners
- instance.addEventListener(...);
- }
- }
- override protected function partRemoved(partName:String, instance:*):void
- {
- super.partRemoved(partName, instance);
- if (partName == SEPARATOR_NAME)
- {
- // A separator is being removed
- // Remove event listeners
- instance.removeEventListener(...);
- }
- }
-
- override protected function detachSkin():void
- {
- for (var i:int = 0; i < separators.length; i++)
- {
- separatorAlphas[i] = separators[i].alpha;
- }
- separators = [];
- super.detachSkin();
- }
- override protected function attachSkin():void
- {
- super.attachSkin();
-
- // create separators from last time
-
- for (var i:int = 0; i < separatorAlphas.length; i++)
- {
- var separator:DisplayObject = createPartInstance(SEPARATOR_NAME);
-
- separator.alpha = separatorAlphas[i];
-
- separators.push(separator);
- skin.addElement(separator);
- }
- }
- }
- }
12. style 皮肤定义了组件的可视部分。皮肤与style的交互,可以有两种方法:binding和重载updateDisplayList()。
Binding 例子
- <s:Skin xmlns:fx="" xmlns:s="library://ns.adobe.com/flex/spark"
- minWidth="21" minHeight="21" alpha.disabled="0.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>
-
- <s:Rect id="fill" left="1" right="1" top="1" bottom="1" radiusX="{getStyle('cornerRadius')}">
- <s:fill>
- <s:LinearGradient rotation="90">
- <s:GradientEntry color="0xFFFFFF"
- color.over="0xBBBDBD"
- color.down="0xAAAAAA"
- alpha="0.85" />
- <s:GradientEntry color="0xD8D8D8"
- color.over="0x9FA0A1"
- color.down="0x929496"
- alpha="0.85" />
- </s:LinearGradient>
- </s:fill>
- </s:Rect>
-
- <s:Label id="labelDisplay"
- textAlign="center"
- verticalAlign="middle"
- maxDisplayedLines="1"
- horizontalCenter="0" verticalCenter="1"
- left="10" right="10" top="2" bottom="2" />
-
- </s:Skin>
updateDisplayList()例子如下
- <?xml version="1.0" encoding="utf-8"?>
- <s:Skin xmlns:fx="" xmlns:s="library://ns.adobe.com/flex/spark"
- minWidth="21" minHeight="21" alpha.disabled="0.5">
-
- <fx:Metadata>[HostComponent("spark.components.Button")]</fx:Metadata>
-
- <fx:Script>
- <![CDATA[
- override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number) : void
- {
- fill.radiusX = getStyle('cornerRadius');
-
- super.updateDisplayList(unscaledWidth, unscaledHeight);
- }
- ]]>
- </fx:Script>
-
- <!-- states -->
- <s:states>
- <s:State name="up" />
- <s:State name="over" />
- <s:State name="down" />
- <s:State name="disabled" />
- </s:states>
-
- <s:Rect id="fill" left="1" right="1" top="1" bottom="1">
- <s:fill>
- <s:LinearGradient rotation="90">
- <s:GradientEntry color="0xFFFFFF"
- color.over="0xBBBDBD"
- color.down="0xAAAAAA"
- alpha="0.85" />
- <s:GradientEntry color="0xD8D8D8"
- color.over="0x9FA0A1"
- color.down="0x929496"
- alpha="0.85" />
- </s:LinearGradient>
- </s:fill>
- </s:Rect>
-
- <s:Label id="labelDisplay"
- textAlign="center"
- verticalAlign="middle"
- maxDisplayedLines="1"
- horizontalCenter="0" verticalCenter="1"
- left="10" right="10" top="2" bottom="2" />
-
- </s:Skin>
下面在style中定义属性cornerRadius
- <?xml version="1.0" encoding="utf-8"?>
- <s:Application xmlns:fx="" xmlns:s="library://ns.adobe.com/flex/spark">
- <fx:Style>
- @namespace s "library://ns.adobe.com/flex/spark";
-
- s|Button
- {
- cornerRadius: 10;
- }
- </fx:Style>
- <s:Button skinClass="GradientSkin" label="hello" />
- </s:Application>
也可以在mxml中内置style,直接在组件中声明style元标签。比如
- [Style(name="cornerRadius", type="Number", format="Length", inherit="no", minValue="0.0")]
- public class Button extends SkinnableComponent
- {
- ...
- }
13 API- SkinnableComponent:
- package spark.components.supportClasses
- {
- /**
- * Name of the skin to use for this component. The skin must be a class that extends
- * the flex.core.Skin class.
- */
- [Style(name="skinClass", type="Class")]
- public class SkinnableComponent extends UIComponent
- {
- //--------------------------------------------------------------------------
- //
- // Properties
- //
- //--------------------------------------------------------------------------
- /**
- * The instance of the skin class for this component instance.
- * This is a read-only property that gets set automomatically when Flex
- * calls the
attachSkin()
method.
- */
- public function get skin():UIComponent;
- //--------------------------------------------------------------------------
- //
- // Methods - Skin/Behavior lifecycle
- //
- //--------------------------------------------------------------------------
- /**
- * Create the skin for the component.
- * You do not call this method directly.
- * Flex calls it automatically when it calls
createChildren()
or
- * the
UIComponent.commitProperties()
method.
- * Typically, a subclass of SkinnableComponent does not override this method.
- *
- *
This method instantiates the skin for the component,
- * adds the skin as a child of the component, and
- * resolves all part associations for the skin
- */
- protected function attachSkin():void;
- /**
- * Destroys and removes the skin for this component.
- * You do not call this method directly.
- * Flex calls it automatically when a skin is changed at runtime.
- *
- * This method removes the skin and clears all part associations.
- *
- *
Typically, subclasses of SkinnableComponent do not override this method.
- */
- protected function detachSkin():void;
- /**
- * Find the skin parts in the skin class and assign them to the properties of the component.
- * You do not call this method directly.
- * Flex calls it automatically when it calls the
attachSkin()
method.
- * Typically, a subclass of SkinnableComponent does not override this method.
- */
- protected function findSkinParts():void;
- /**
- * Clear out references to skin parts.
- * You do not call this method directly.
- * Flex calls it automatically when it calls the
detachSkin()
method.
- *
- *
Typically, subclasses of SkinnableComponent do not override this method.
- */
- protected function clearSkinParts():void;
- //--------------------------------------------------------------------------
- //
- // Methods - Parts
- //
- //--------------------------------------------------------------------------
- /**
- * Called when a skin part is added.
- * You do not call this method directly.
- * For static parts, Flex calls it automatically when it calls the
attachSkin()
method.
- * For dynamic parts, Flex calls it automatically when it calls
- * the
createDynamicPartInstance()
method.
- *
- *
Override this function to attach behavior to the part.
- * If you want to override behavior on a skin part that is inherited from a base class,
- * do not call the
super.partAdded()
method.
- * Otherwise, you should always call the
super.partAdded()
method.
- *
- * @param partname The name of the part.
- *
- * @param instance The instance of the part.
- */
- protected function partAdded(partName:String, instance:Object):void;
- /**
- * Called when an instance of a skin part is being removed.
- * You do not call this method directly.
- * For static parts, Flex calls it automatically when it calls the
detachSkin()
method.
- * For dynamic parts, Flex calls it automatically when it calls
- * the
removeDynamicPartInstance()
method.
- *
- *
Override this function to remove behavior from the part.
- *
- * @param partname The name of the part.
- *
- * @param instance The instance of the part.
- */
- protected function partRemoved(partName:String, instance:Object):void;
- //--------------------------------------------------------------------------
- //
- // Methods - Dynamic Parts
- //
- //--------------------------------------------------------------------------
- /**
- * Create an instance of a dynamic skin part.
- * Dynamic skin parts should always be instantiated by this method,
- * rather than directly by calling the
newInstance()
method on the factory.
- * This method creates the part, but does not add it to the display list.
- * The component must call the
Group.addElement()
method, or the appropriate
- * method to add the skin part to the display list.
- *
- * @param partName The name of the part.
- *
- * @return The instance of the part, or null if it cannot create the part.
- */
- protected function createDynamicPartInstance(partName:String):Object;
- /**
- * Remove an instance of a dynamic part.
- * You must call this method before a dynamic part is deleted.
- * This method does not remove the part from the display list.
- *
- * @param partname The name of the part.
- *
- * @param instance The instance of the part.
- */
- protected function removeDynamicPartInstance(partName:String, instance:Object):void;
- /**
- * Returns the number of instances of a dynamic part.
- *
- * @param partName The name of the dynamic part.
- *
- * @return The number of instances of the dynamic part.
- */
- protected function numDynamicParts(partName:String):int;
- /**
- * Returns a specific instance of a dynamic part.
- *
- * @param partName The name of the dynamic part.
- *
- * @param index The index of the dynamic part.
- *
- * @return The instance of the part, or null if it the part does not exist.
- */
- protected function getDynamicPartAt(partName:String, index:int):Object;
- //--------------------------------------------------------------------------
- //
- // Skin states support
- //
- //--------------------------------------------------------------------------
- /**
- * Returns the name of the state to be applied to the skin. For example, a
- * Button component could return the String "up", "down", "over", or "disabled"
- * to specify the state.
- *
- *
A subclass of SkinnableComponent must override this method to return a value.
- *
- * @return A string specifying the name of the state to apply to the skin.
- */
- protected function getCurrentSkinState():String;
- /**
- * Marks the component so that the new state of the skin is set
- * during a later screen update.
- */
- public function invalidateSkinState():void;
- }
- }
参考文献1.Spark Skinning (including SkinnableComponent) - Functional and Design Specification. http://www.cnblogs.com/fxair/archive/2010/05/25/1743638.html
2.