技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

FlexComponentを作ってみる

マズ、基本的なものを作ってみる。
サンプルには、ChakListを用意してみた。
最初に、MXML で作成してみる。

<?xml version="1.0" encoding="utf-8"?>
<mx:List xmlns:mx="http://www.adobe.com/2006/mxml">
  <mx:itemRenderer>
    <mx:Component>
      <mx:HBox>
        <mx:CheckBox id="a_chk"
          label="{data.label}"
          selected="{data.selected}"
          change="data.selected = a_chk.selected"/>
      </mx:HBox>
    </mx:Component>
  </mx:itemRenderer>
</mx:List>

シンプル。
で、これを使うコード的には

<?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"
               backgroundColor="#00ccff"
               creationComplete="init()">
	
	<fx:Script>
        <![CDATA[
        import mx.collections.ArrayCollection;
        import net.azalea.white.components.CheckBoxMXML;
        
        public function init():void
        {
            var collection:ArrayCollection = new ArrayCollection([
                { label:"label1", selected:false },
                { label:"label2", selected:false },
                { label:"label3", selected:false },
                { label:"label4", selected:false },
                { label:"label5", selected:false },
                { label:"label6", selected:false }
            ]);
            
            var check:CheckBoxMXML
                = new net.azalea.white.components.CheckBoxMXML();
            check.dataProvider = collection;
            
            this.canvas.addChild(check);
        }
        ]]>
    </fx:Script>
    <mx:Canvas id="canvas" width="100%" height="100%">
    </mx:Canvas>
</s:Application>

てなぐあい。
MXMLでも書けるけど、ビルダじゃないからで切り貼りできなくて面倒なので慣れ親しんだコードw
普通にコンポーネントとして扱うことはできる。
実行結果はこんな感じ。

これを、コードで実装してみる。


はじめに、itemRenderer に設定するカスタムHBoxを作成する。

package net.azalea.white.components 
{
    import mx.containers.HBox;
    import mx.controls.CheckBox;
    import mx.binding.utils.BindingUtils;
    import mx.binding.utils.ChangeWatcher;
    import mx.core.Container;
    import mx.core.UIComponent;
	
	/**
     * ...
     * @author azalea
     */
    public class InternalHBox extends HBox
    {
        protected var internalCheckBox:CheckBox;
        
        public function InternalHBox() 
        {
            super();
        }
        
        override protected function createChildren():void 
        {
            super.createChildren();
            internalCheckBox = new CheckBox();
            
            this.addChild(internalCheckBox);
        }
        
        private var dataChanged:Boolean = false;
        
        override public function get data():Object { return super.data; }
        
        override public function set data(value:Object):void 
        {
            super.data = value;
            dataChanged = true;
            
            this.invalidateProperties();
            this.invalidateSize();
            this.invalidateDisplayList();
        }
        
        private var watcher:ChangeWatcher = null;
        
        override protected function commitProperties():void 
        {
            super.commitProperties();
            
            if (dataChanged)
            {
                dataChanged = false;
                
                if(this.data.hasOwnProperty("label"))
                    this.internalCheckBox.label = this.data["label"];
                
                if (this.data.hasOwnProperty("selected"))
                {
                    this.internalCheckBox.selected = this.data["selected"];
                    
                    // データ入れ替え時にバインドは除去する
                    if (watcher)
                        watcher.unwatch();
                    
                    // 手動バインド
                    watcher = BindingUtils.bindProperty(
                        this.data, "selected", this.internalCheckBox, "selected");
                }
            }
        }
        
        override protected function measure():void 
        {
            // 子のサイズから、自身のサイズを確定する。
            var newWidth:Number = this.internalCheckBox.measuredWidth;
            var newHeight:Number = this.internalCheckBox.measuredHeight;
            
            this.internalCheckBox.setActualSize(newWidth, newHeight);
            
            // HBox のサイズ計算は標準動作に任せる
            super.measure();
        }
        
        override protected function updateDisplayList(
            unscaledWidth:Number, unscaledHeight:Number):void 
        {
            super.updateDisplayList(unscaledWidth, unscaledHeight);
            
            // 特にオーバーライドの意味はないけど、位置調整を明示
            this.internalCheckBox.x = 0;
            this.internalCheckBox.y = 0;
        }
    }
}

Flex 標準のお作法に則ってやってみた。
多分、きちっとmeasureを作りこむなら、HBoxである意味はない筈。*1
そして、List の itemRenderer に直接突っ込んでもいいけど、あえてレンダラを固定にするためにカスタムクラスを作った。

package net.azalea.white.components 
{
	import mx.controls.List;
    import mx.core.ClassFactory;
	
	/**
     * ...
     * @author azalea
     */
    public class CheckListCode extends List
    {
        
        public function CheckListCode() 
        {
            super();
        }
        
        override protected function createChildren():void 
        {
            this.itemRenderer = new ClassFactory(InternalHBox);
            super.createChildren();
        }
    }
}

itemRenderer の設定タイミングが違う気がしないではないが気にしないw

呼び出し側は、基本的に変わらずに、

<?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"
               backgroundColor="#00ccff"
               creationComplete="init()">
	
	<fx:Script>
        <![CDATA[
        import mx.collections.ArrayCollection;
        import net.azalea.white.components.CheckListCode;
        
        public function init():void
        {
            var collection:ArrayCollection = new ArrayCollection([
                { label:"label1", selected:false },
                { label:"label2", selected:false },
                { label:"label3", selected:false },
                { label:"label4", selected:false },
                { label:"label5", selected:false },
                { label:"label6", selected:false }
            ]);
            
            var check:CheckListCode
                = new CheckListCode();
            check.dataProvider = collection;
            
            this.canvas.addChild(check);
        }
        ]]>
    </fx:Script>
    <mx:Canvas id="canvas" width="100%" height="100%">
    </mx:Canvas>
</s:Application>

commitProperties → measure →updateDisplayList の順に処理されること、それぞれで、プロパティ値確定、各種コンポーネント、画面サイズ確定、表示位置確定の機能があることをメモ。
なので、updateDisplayList に自分の label プロパティを設定するようなロジックがあると、無限ループで落ちるっぽかったりもする。*2
一気に手間が増えるが、個人的にはこっちのほうが何してるかわかるので好き。*3

*1:本当なら、CheckBoxのサブクラスでも良かったと思われる

*2:もっともComboBoxのソース見ると、updateDisplayListの中で内部表示用のtextFieldにprompt渡したりと微妙なコトしてるんだが

*3:わかってて隠蔽するのと、知らずに隠蔽されたものを使うのとでは、意味が違う。