Flex4 Skinning 2: 皮肤协议
上一篇随笔中笔者介绍了如何为按钮制作一个简单的自定义皮肤,接下来分析一下皮肤文件的组成部分,并对皮肤协议(skinning contract)中各个部分进行详细介绍。
首先可以看到根标签为Skin, 接下来是HostComponent元数据,这个可以在我们新建文件的时候指定,可以参照上一篇随笔的图1。
<fx:Metadata> [HostComponent("spark.components.Button")] </fx:Metadata>
接下来就是states。
<!-- states --> <s:states> <s:State name="disabled" /> <s:State name="down" /> <s:State name="over" /> <s:State name="up" /> </s:states>
来查看一下按钮(spark.components.Button)组件的源代码,准确说应该是按钮(spark.components.Button)类的父类按钮基类(spark.components.supportClasses.ButtonBase), 找到元数据中带有SkinState元数据的部分,可以看到该类具有四个skin state: up, over, down disabled, 这与在皮肤文件中定义的states部分相对应。代码如下:
//-------------------------------------- // Skin states //-------------------------------------- /** * Up State of the Button * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ [SkinState("up")] /** * Over State of the Button * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ [SkinState("over")] /** * Down State of the Button * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ [SkinState("down")] /** * Disabled State of the Button * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ [SkinState("disabled")]
States是Flex4中skinning contract的重要组成部分。皮肤(skin)文件和相对应的控件组件(component)相互交互用到的正是skinning contract. 如同Button有四个state一样,每个skinning component都有一系列相对应的state, 可以通过component当前所处的state来对外观进行不同的设置。
组件的includeIn和excludeFrom属性为不同state的外观定义提供了很大便利。通过这两个属性可以在某特定的state下设置该state需要的外观。另外值得注意的是也可以通过“属性名.state=属性值”来进行更进一步的设置。如:
<s:SolidColor color="0x77CC22" color.over="0x92D64E" color.down="0x67A41D" />
以上代码段表示当按钮处于over state的时候,颜色为"0x92D64E",其它state时颜色为"0x77CC22"。
SkinState元数据为皮肤文件中定义哪些state做了直接说明,如果皮肤中没有定义相关的state, 而且这个state被设置为required=true, 那么编译就会报错。
子类会从父类中集成state, 比如前面提到的四个skin state: up, over, down, disabled的定义并不存在于spark.components.Button中,而是在ButtonBase中。不过我们可以通过Exclude元数据对其进行过滤。
在开发过程中,如果改变state, 需要用到component的getCurrentSkinState()和invalidateSkinState()方法。getCurrentSkinState()用来追踪component的各个属性,并确定skin应该处于的state. invalidateSkinState()方法会使改变前的state失效并且根据getCurrentSkinState()方法返回值来设置当前的state.
有时候skin需要引用相关component的内容。如果设置了HostComponent元数据,那么在skin里面就可以直接调用。在skin和component之间交互数据有两种方式。推式(push method)是指component把数据推入到skin中。相反的,拉式(pull method)是指skin向component索要属性值数据。关于这两种数据传输方式并没有固定的规定,但是一般情况下,如果component中定义SkinPart为required=true, 则采用推式(push method)。
接下来我们可以在spark.components.supportClass.ButtonBase类定义中可以找到标有SkinPart的成员变量labelDisplay, 代码如下:
//-------------------------------------------------------------------------- // // Skin parts // //-------------------------------------------------------------------------- [SkinPart(required="false")] /** * A skin part that defines the label of the button. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var labelDisplay:TextBase;
Skin part也是skin contract的重要组成部分。任何复杂的component都是由简单的组合而成。比如前面提到的按钮,就是由文本和矩形组成。文本就是按钮的skin part. 它由component声明,可以是可选的。定义的方式是使用SkinPart元数据,可以把这个元数据标记在任何公共的属性上,而属性的名称就成了这个part的名称。比如在按钮当中,labelDisplay就是一个SkinPart, 它被定义为可选的,类型为TextBase.
在component中定义了skin part后,需要在skin文件中进行对应的实现,比如我们可以将上一篇随笔中定义的label的id属性设置为labelDisplay, 这样Flex就会通过id属性自动将这个label与Button的labelDisplay相匹配。
将皮肤文件的Label部分改成如下代码所示:
<!-- text --> <s:Label id="labelDisplay" text="{hostComponent.label}" color="0x131313" textAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="1" left="12" right="12" top="6" bottom="6" />
可以发现,此时skin文件要想引用对应的component中的数据只需要使用hostComponent属性就可以了,这是因为我们在最开始就已经对元数据HostComponent进行了设置。修改相应的主应用程序,代码如下所示:
<?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" xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600"> <fx:Declarations> <!-- Place non-visual elements (e.g., services, value objects) here --> </fx:Declarations> <fx:Style> @namespace s "library://ns.adobe.com/flex/spark"; @namespace mx "library://ns.adobe.com/flex/mx"; s|Button { skinClass:ClassReference("com.guyue.skins.SimpleButtonSkin"); } </fx:Style> <s:layout> <s:VerticalLayout /> </s:layout> <s:Button label="Simple Button" /> <s:Button label="I used to be a button without customerized skin. " /> </s:Application>
再次运行程序,可以发现现在两个按钮公用同样的皮肤,而且skin的定义在主应用程序端是完全透明的,这样外观和逻辑就实现了分离。如图3.
Figure 3. 使用样式可以同时为多个按钮设置皮肤
上面提到的TextBase是确定的类型,而且在声明周期中只会存在唯一的实例,称为静态的。当加载静态SkinPart的皮肤时,SkinnableComponent基类负责将所有的静态part从skin加载到component中。加载完毕之后,component就可以直接使用这些实例了。当part加载的时候,partAdded()会被调用,当part移除的时候partRemoved()方法会被调用。因此我们可以在这两个方法中添加相关事件处理逻辑。
/** * @private */ override protected function partAdded(partName:String, instance:Object):void { super.partAdded(partName, instance); if (instance == labelDisplay) { labelDisplay.addEventListener("isTruncatedChanged", labelDisplay_isTruncatedChangedHandler); // Push down to the part only if the label was explicitly set if (_content !== undefined) labelDisplay.text = label; } } /** * @private */ override protected function partRemoved(partName:String, instance:Object):void { super.partRemoved(partName, instance); if (instance == labelDisplay) { labelDisplay.removeEventListener("isTruncatedChanged", labelDisplay_isTruncatedChangedHandler); } }
以上便是Button类中相关的代码,可以看出我们能够动态的为component中skin part添加或减少相关事件监听器。
但是有些时候我们需要动态的使用多个SkinPart的实例,比如spark.components.supportClasses.SliderBase这个类的所有子类的dataTip属性。这种SkinPart称为动态的。具体的代码如下所示:
//-------------------------------------------------------------------------- // // Skin parts // //-------------------------------------------------------------------------- [SkinPart(required="false", type="mx.core.IDataRenderer")] /** * A skin part that defines a dataTip that displays a formatted version of * the current value. The dataTip appears while the thumb is being dragged. * This is a dynamic skin part and must be of type IFactory. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var dataTip:IFactory;
可以看出dataTip属性的类型为Ifactory, 这表示开发人员可以动态的在component中对其实例利用createDynamicPartInstance()方法初始化,利用removeDynamicPartInstance()方法来移除。相应的partAdded()和partRemoved()方法与静态SkinPart处理相似。在一个component可以有多个动态part实例,何时调用createDynamicPartInstance()完全由开发人员根据需要来决定。
卸载skin基本上是自动进行的。程序运行时skin发生变化的时候就会调用detachSkin()方法。所有skin part相关的partRemoved()方法也会自动调用。开发人员可以在这两个方法里添加相应的事件监听。
有些复杂的component 可能需要在skin创建后某个不确定的时间与skin part进行交互。Component需要知道partAdded()方法可能在skin初始化很久以后才会被调用。这样的skin part被称为延迟的(deferred part)。一个典型的例子就是TabNavigator, 当用户点击了某个Tab之后,component才会知道新的part然后调用partAdded()方法。
图4是Adobe官方给出的skin parts生命周期。
Figure 4.
本篇随笔中提到的state, skin part, data构成了skin contract. 他们共同实现了component及其skin的定义和交互。
人每天要做三件事,第一件是微笑,第二件是欢笑,第三件是大笑。