上一篇随笔中笔者介绍了如何为按钮制作一个简单的自定义皮肤,接下来分析一下皮肤文件的组成部分,并对皮肤协议(skinning contract)中各个部分进行详细介绍。
首先可以看到根标签为Skin, 接下来是HostComponent元数据,这个可以在我们新建文件的时候指定,可以参照上一篇随笔的图1。
2 | [HostComponent( "spark.components.Button" )] |
接下来就是states。
3 | <s:State name= "disabled" /> |
4 | <s:State name= "down" /> |
5 | <s:State name= "over" /> |
来查看一下按钮(spark.components.Button)组件的源代码,准确说应该是按钮(spark.components.Button)类的父类按钮基类(spark.components.supportClasses.ButtonBase), 找到元数据中带有SkinState元数据的部分,可以看到该类具有四个skin state: up, over, down disabled, 这与在皮肤文件中定义的states部分相对应。代码如下:
43 | [SkinState( "disabled" )] |
States是Flex4中skinning contract的重要组成部分。皮肤(skin)文件和相对应的控件组件(component)相互交互用到的正是skinning contract. 如同Button有四个state一样,每个skinning component都有一系列相对应的state, 可以通过component当前所处的state来对外观进行不同的设置。
组件的includeIn和excludeFrom属性为不同state的外观定义提供了很大便利。通过这两个属性可以在某特定的state下设置该state需要的外观。另外值得注意的是也可以通过“属性名.state=属性值”来进行更进一步的设置。如:
1 | <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, 代码如下:
07 | [SkinPart(required= "false" )] |
17 | 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部分改成如下代码所示:
2 | <s:Label id= "labelDisplay" text= "{hostComponent.label}" color= "0x131313" |
3 | textAlign= "center" verticalAlign= "middle" |
4 | horizontalCenter= "0" verticalCenter= "1" |
5 | left= "12" right= "12" top= "6" bottom= "6" /> |
可以发现,此时skin文件要想引用对应的component中的数据只需要使用hostComponent属性就可以了,这是因为我们在最开始就已经对元数据HostComponent进行了设置。修改相应的主应用程序,代码如下所示:
01 | <?xml version= "1.0" encoding= "utf-8" ?> |
05 | minWidth= "955" minHeight= "600" > |
08 | <!-- Place non-visual elements (e.g., services, value objects) here --> |
15 | skinClass:ClassReference( "com.guyue.skins.SimpleButtonSkin" ); |
23 | <s:Button label = "Simple Button" /> |
25 | <s:Button label = "I used to be a button without customerized skin. " /> |
再次运行程序,可以发现现在两个按钮公用同样的皮肤,而且skin的定义在主应用程序端是完全透明的,这样外观和逻辑就实现了分离。如图3.
Figure 3. 使用样式可以同时为多个按钮设置皮肤
上面提到的TextBase是确定的类型,而且在声明周期中只会存在唯一的实例,称为静态的。当加载静态SkinPart的皮肤时,SkinnableComponent基类负责将所有的静态part从skin加载到component中。加载完毕之后,component就可以直接使用这些实例了。当part加载的时候,partAdded()会被调用,当part移除的时候partRemoved()方法会被调用。因此我们可以在这两个方法中添加相关事件处理逻辑。
04 | override protected function partAdded(partName: String , instance: Object ): void |
06 | super .partAdded(partName, instance); |
08 | if (instance == labelDisplay) |
10 | labelDisplay.addEventListener( "isTruncatedChanged" , |
11 | labelDisplay_isTruncatedChangedHandler); |
14 | if (_content !== undefined ) |
15 | labelDisplay.text = label ; |
22 | override protected function partRemoved(partName: String , instance: Object ): void |
24 | super .partRemoved(partName, instance); |
26 | if (instance == labelDisplay) |
28 | labelDisplay.removeEventListener( "isTruncatedChanged" , |
29 | labelDisplay_isTruncatedChangedHandler); |
以上便是Button类中相关的代码,可以看出我们能够动态的为component中skin part添加或减少相关事件监听器。
但是有些时候我们需要动态的使用多个SkinPart的实例,比如spark.components.supportClasses.SliderBase这个类的所有子类的dataTip属性。这种SkinPart称为动态的。具体的代码如下所示:
07 | [SkinPart(required= "false" , type= "mx.core.IDataRenderer" )] |
19 | 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的定义和交互。