Flex 4 Spark Skin ライフサイクル考察 #01

2012.02.09

Flex 4 Spark コンポーネントのライフサイクルメソッドである partAdded()、partRemoved() という 2 つのメソッドは、動的に生成 / 破棄を繰り返すようなコンポーネントを作成するときに知っておくべきメソッドです。
正しい使い方を知ることによって、生成 / 破棄時に必要な処理を適切に実装することが可能になります。

メソッド解説

partAdded() メソッドは、スキンパーツが追加されたときに実行されるメソッドです。createChildren() メソッド代わりに使う方も少なくないと思います。
対して partRemoved() メソッドは、通常あまり使われることはありません。このメソッドは、スキンパーツのインスタンスが削除されるときに呼び出されますが、ホストとなるコンポーネントを削除しただけでは実行されないからです。

どのようなシーンで使用する ( される ) のか

partAdded() と partRemoved() メソッドが役立つケースは、コンポーネントのスキンが動的に切り替わる場合です。
たとえば、将棋の駒が成駒になるように、本体 ( ホストコンポーネント ) はそのままで、表面 ( スキン ) の見た目のみが変わるような機能を実装する場合に必要になります。

実例「着せ替えコンポーネント」

画像のような着せ替え機能を実装した人を表す SkinnableComponent(Person.as) を用意して、専用のスキンを 3 つ用意します。 (PersonSkinA, PersonSkinB, PersonSkinC)

各スキンの保持スキンパーツは以下のとおりです。

  • PersonSkinA
    • 帽子 (hat)
  • PersonSkinB
    • 帽子 (hat)
    • シャツ (shirt)
  • PersonSkinC
    • 帽子 (hat)
    • シャツ (shirt)
    • パンツ (pants)

これら 3 つのスキンを切り替えることによって、partAdded() と partRemoved() メソッドが実行されます。( 一見、同じように見える hat も別のインスタンスに切り替わります )

ここで重要なことは、ホストコンポーネントで [SkinPart] …と定義されているスキンパーツの変数の中身は、空 (null) でも構わないという考え方です。

コード

画像と見た目は変わりますが、スキンの切り替えとライフサイクルメソッドの挙動が確認できるサンプルを掲載します。( サンプルファイル : ConsistSample20120209.zip )

Person.as

package jp.classmnethod.component {
    import flash.events.MouseEvent;
    import spark.components.Button;
    import spark.components.supportClasses.SkinnableComponent;
    public class Person extends SkinnableComponent {
        protected var tmpHatSkinClass     :Class;
        protected var hatSkinClass        :Class;
        protected var hatSkinClassChanged :Boolean;
        [SkinPart]
        public var hat   :Button;
        [SkinPart]
        public var shirt :Button;
        [SkinPart]
        public var pants :Button;
        public function Person() {
            super();
            setStyle("skinClass", PersonSkinA);
        }
        protected override function measure():void {
            super.measure();
            var w:int = 100;
            var h:int = 100;
            measuredWidth     = w;
            measuredMinWidth  = w;
            measuredHeight    = h
            measuredMinHeight = h;
        }
        protected override function partAdded(partName:String, instance:Object):void {
            super.partAdded(partName, instance);
            echo("partAdded : " + instance.id);
            if(hat == instance) {
                if(tmpHatSkinClass != null) {
                    hat.setStyle("skinClass", tmpHatSkinClass);
                    tmpHatSkinClass = null;
                }
                hat.addEventListener(MouseEvent.CLICK, hatClickHandler);
            }
            else if(shirt == instance) {
                shirt.addEventListener(MouseEvent.CLICK, shirtClickHandler);
            }
            else if(pants == instance) {
                pants.addEventListener(MouseEvent.CLICK, pantsClickHandler);
            }
        }
        protected override function partRemoved(partName:String, instance:Object):void {
            super.partRemoved(partName, instance);
            echo("partRemoved : " + instance.id);
            if(hat == instance) {
                hat.removeEventListener(MouseEvent.CLICK, hatClickHandler);
                tmpHatSkinClass = hat.getStyle("skinClass");
            }
            else if(shirt == instance) {
                shirt.removeEventListener(MouseEvent.CLICK, shirtClickHandler);
            }
            else if(pants == instance) {
                pants.removeEventListener(MouseEvent.CLICK, pantsClickHandler);
            }
        }
        protected override function commitProperties():void {
            super.commitProperties();
            if(hatSkinClassChanged) {
                hatSkinClassChanged = false;
                if(hat != null &&  hatSkinClass != null) {
                    hat.setStyle("skinClass", hatSkinClass);
                }
            }
        }
        protected function hatClickHandler(event:MouseEvent):void {
            echo("hat Click");
            var button :Button = event.currentTarget as Button;
            var clazz  :Class  = button.getStyle("skinClass");
            if(clazz != null) {
                if(clazz == HatButtonSkinA) {
                    clazz = HatButtonSkinB;
                }
                else if(clazz == HatButtonSkinB) {
                    clazz = HatButtonSkinC;
                }
                else if(clazz == HatButtonSkinC) {
                    clazz = HatButtonSkinA;
                }
                else {
                    clazz = HatButtonSkinA;
                }
                setHatSkinClass(clazz);
            }
        }
        protected function shirtClickHandler(event:MouseEvent):void {
            echo("shirt Click");
        }
        protected function pantsClickHandler(event:MouseEvent):void {
            echo("pants Click");
        }
        protected function setHatSkinClass(value:Class):void {
            if(hatSkinClass != value) {
                hatSkinClass = value;
                hatSkinClassChanged = true;
                invalidateProperties();
            }
        }
    }
}

echo.as

package {
import flash.utils.getTimer;
import mx.core.FlexGlobals;
public function echo(str:String):void {
    FlexGlobals.topLevelApplication.log
        = getTimer().toString() + " : " + String(str + "\n") + FlexGlobals.topLevelApplication.log;
}
}

PersonSkinA.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"
>

<fx:Metadata>
    [HostComponent("jp.classmnethod.component.Person")]
</fx:Metadata>

<s:Rect left="0" top="0" right="0" bottom="0">
    <s:fill>
        <s:SolidColor color="0xdedede" />
    </s:fill>
</s:Rect>

<s:Group left="0" top="0" right="0" bottom="0">
    <s:layout>
        <s:VerticalLayout />
    </s:layout>
    <s:Button id="hat" width="100" height="30" label="hat" skinClass="HatButtonSkinA" />
</s:Group>

</s:Skin>

PersonSkinB.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"
>

<fx:Metadata>
    [HostComponent("jp.classmnethod.component.Person")]
</fx:Metadata>

<s:Rect left="0" top="0" right="0" bottom="0">
    <s:fill>
        <s:SolidColor color="0xdedede" />
    </s:fill>
</s:Rect>

<s:Group left="0" top="0" right="0" bottom="0">
    <s:layout>
        <s:VerticalLayout gap="5" />
    </s:layout>
    <s:Button id="hat"   width="100" height="30" label="hat" skinClass="HatButtonSkinA" />
    <s:Button id="shirt" width="100" height="30" label="shirt" />
</s:Group>

</s:Skin>

PersonSkinC.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"
>

<fx:Metadata>
    [HostComponent("jp.classmnethod.component.Person")]
</fx:Metadata>

<s:Rect left="0" top="0" right="0" bottom="0">
    <s:fill>
        <s:SolidColor color="0xdedede" />
    </s:fill>
</s:Rect>

<s:Group left="0" top="0" right="0" bottom="0">
    <s:layout>
        <s:VerticalLayout gap="5" />
    </s:layout>
    <s:Button id="hat"   width="100" height="30" label="hat"   fontWeight="bold" skinClass="HatButtonSkinA" />
    <s:Button id="shirt" width="100" height="30" label="shirt" fontWeight="bold" />
    <s:Button id="pants" width="100" height="30" label="pants" fontWeight="bold" />
</s:Group>

</s:Skin>

Main.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"
xmlns:c  = "jp.classmnethod.component.*"
fontSize = "15"
>

<fx:Script>
<![CDATA[
import spark.events.IndexChangeEvent;
[Bindable]
public var log:String="";
protected override function createChildren():void {
    super.createChildren();
    skinBar.addEventListener(IndexChangeEvent.CHANGE, skinBarChangeHandler);
    personBar.addEventListener(IndexChangeEvent.CHANGE, personBarChangeHandler);
}
protected function skinBarChangeHandler(event:IndexChangeEvent):void {
    var newIndex:int = event.newIndex;
    switch(newIndex) {
        case 0: p.setStyle("skinClass", PersonSkinA); break;
        case 1: p.setStyle("skinClass", PersonSkinB); break;
        case 2: p.setStyle("skinClass", PersonSkinC); break;
    }
}
protected function personBarChangeHandler(event:IndexChangeEvent):void {
    var newIndex:int = event.newIndex;
    switch(newIndex) {
        case 0: vg.addElementAt(p,1); break;
        case 1: vg.removeElement(p); break;
    }
}
]]>
</fx:Script>

<s:HGroup verticalCenter="0" horizontalCenter="0" gap="10">

    <s:TextArea width="300" height="300" editable="false" text="{log}" />

    <s:VGroup id="vg" width="200" height="300" verticalAlign="middle" horizontalAlign="center" gap="10">

        <s:Label text="Person.as" fontSize="18" />

        <c:Person id="p" skinClass="PersonSkinA" />

        <s:ButtonBar id="skinBar" width="200" requireSelection="true">
            <s:dataProvider>
                <s:ArrayList>
                    <fx:String>A Skin</fx:String>
                    <fx:String>B Skin</fx:String>
                    <fx:String>C Skin</fx:String>
                </s:ArrayList>
            </s:dataProvider>
        </s:ButtonBar>

        <s:ButtonBar id="personBar" width="200" requireSelection="true">
            <s:dataProvider>
                <s:ArrayList>
                    <fx:String>add</fx:String>
                    <fx:String>remove</fx:String>
                </s:ArrayList>
            </s:dataProvider>
        </s:ButtonBar>

    </s:VGroup>
</s:HGroup>

</s:Application>

出力結果

出力結果は次の通り

[SWF]http://public-blog-dev.s3.amazonaws.com/wp-content/uploads/2012/02/ConsistSample20120209.swf,640,480[/SWF]