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的定义和交互。

posted @ 2010-11-03 23:34  spoony  阅读(1744)  评论(0编辑  收藏  举报