Flex 4에 대해서 자료를 모으고 공부하는 중에 매우 괜찮은 블로그를 찾았다.

http://www.hulstkamp.com/

이 사람 블로그에 가면 Flex 4기반 커스텀 컴포넌트를 만든 예제들이 정말 많다. 한동안 이 사람 블로그와 친해져야 겠다는 생각이 들었다. ㅎㅎ

올라온 자료중에 Gumbo(4.0.0.4932) 버전으로 만든 Knob Button 예제가 있었는데 Flex 4 SDK Beta버전이 정식으로 나오기 전이라 바로 실행할 수 없었다. 그래서 소스를 보면서 마이그레이션 작업을 해보았다. 아래 실행 예제는 바로 이 작업의 결과물이다.

위 프로그램은 Knob Button에 대한 데모이다. 마우스로 돌려볼 수 있다.


마이그레이션 하면서 Spark 기반 커스텀 컴포넌트를 제작하는 법에 더욱 익숙해질 수 있었다.

Flex 3의 Halo 기반 컴포넌트는 스킨이 그래픽 기반만 지원했는데 Flex 4의 Spark기반 컴포넌트들은 스킨이 그래픽적 요소 뿐 아니라, 상태변화(state), 다른 컴포넌트 배치, 데이타 표현등이 된다. 또한 FXG를 지원하고 MXML형태로 스킨을 만들 수 있기 때문에 그 확장력이 무궁무진하게 되었다. Halo 컴포넌트의 경우에는 상태변화 바꾸거나 내부 컴포넌트 배치만 달라져도 컴포넌트 자체를 확장해서 다시 만들어야 하는 불편함이 있었지만 Spark컴포넌트는 큰 변경없이 커스텀 스킨만 제작하는 것만으로도 충분히 해결할 수 있게 되었다. 이 내용은 여기에 올려놓은 소스 또는 "Spark DropDownList 사용하기"를 보면 알 수 있을 것이다.

Spark 컨테이너의 경우에는 Layout까지 동적으로 변경할 수 있도록 만들어져서 언제든지 다른 Layout으로 바꿀 수 있고 또는 커스텀 Layout를 만들어 쓸 수도 있다. 반면 Halo 기반 컨테이너는 컨테이너의 Layout을 바꾸기 위해 기존 컨테이너를 다시 확장하거나 새로 만들어야만 했다. 이 내용에 대해서는 "Spark 컨테이너의 Layout, Scrolling, Viewport 소개"를 읽어보길 바란다.

CSS는 정말 획기적으로 많이 추가 되었다. 기존 Class, Type Selector밖에 없었는데 Flex 4로 넘어오면서 ID, Descendant, Pseudo, Mutiple Class등 다양한 Selector가 추가되었다. 이에 대한 글은 "Flex 4의 CSS"를 참고하길 바란다.

아래는 위 프로그램의 소스이다. 참고바란다.


개발 환경은 Flash Builder 4 Beta 1 이다. Flash Builder는 아래 링크에서 다운 받을 수 있다.

InsideRIA에 올라온 Getting started with Spark skins 의 글을 읽고 따라해보았다.

 

아래는 Skin 소스이다. com.jidolstar.skins.MyButtonSkin으로 이름을 지었다.

<?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" 
	xmlns:mx="library://ns.adobe.com/flex/halo" 
	>
	<fx:Metadata>
		[HostComponent("spark.components.Button")]
	</fx:Metadata>
	
	<s:states>
		<mx:State name="up"/>
		<mx:State name="down"/>
		<mx:State name="over"/>
		<mx:State name="disabled"/>
	</s:states>
	
	<s:filters>
		<s:DropShadowFilter quality="3"
			alpha="0.5" alpha.over="0.3"
			distance="5" distance.over="15"
			blurX.over="15" blurY.over="15"
			inner.down="true"
		/>
	</s:filters>
	
	<s:Rect left="0" right="0" top="0" bottom="0" radiusX="5" radiusY="5">
		<s:fill>
			<mx:LinearGradient rotation="90">
				<mx:entries>
					<mx:GradientEntry color="#0a5c00" ratio="0.00"/>
					<mx:GradientEntry color="#84a381" ratio="0.50" ratio.over="0.25"/>
					<mx:GradientEntry color="#0a5c00" ratio="0.80"/>
				</mx:entries>
			</mx:LinearGradient>
		</s:fill>
	</s:Rect>
	
	<s:SimpleText id="labelElement" color="#ffffff"
		horizontalCenter="0" verticalCenter="0"
     	left="10" right="10" top="5" bottom="5"
     />	
	
	<s:layout>
		<s:BasicLayout/>
	</s:layout>
</s:Skin>

 

아래는 위 Skin을 사용한 소스이다.

<?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/halo" 
	minWidth="500" minHeight="400"
	>
	<s:Button label="확인" skinClass="com.jidolstar.skins.MyButtonSkin"/>
</s:Application>

 

코딩하면서 느끼는 것이지만 Flex 4는 Flex 3하고 너무 다르다. ㅎㅎ  

스킨 부분에 있어서 Flex 3에서는 ProgrammaticSkin을 기반으로 Skin을 ActionScript 코드로 만들었는데 Flex 4부터는 MXML로 만들 수 있도록 했다. 이 부분이 가장 크게 달라진거다.

 

위의 Skin 코드를 살펴보면 몇가지 재미있는 점을 발견할 수 있다. 살펴보자.

 

먼저 HostComponent 메타 데이타를 볼 수 있다. Flex 4에서 나온 새로운 메타 데이타이다. 이것은 이 스킨이 어떤 컴포넌트가 Host인가 명시적으로 지칭해주는 역할을 한다. Button은 ButtonBase를 확장해서 만들었고 ButtonBase는 SkinnableComponent를 확장해서 만들어졌는데 HostComponent가 쓰이는 부분은 SkinnableComponent인 듯 보인다. 아래는 SkinnableComponent내에 정의된 attachSkin() 메소드이다. 이것은 createChildren() 혹은 commitProperties()가 호출될때 이 메소드가 호출되는데 skinClass 스타일이 바뀔 때 Skin을 붙여주는 역할을 담당한다.

 

    /**
     *  Create the skin for the component. 
     *  You do not call this method directly. 
     *  Flex calls it automatically when it calls createChildren() or  
     *  the UIComponent.commitProperties() method.
     *  Typically, a subclass of SkinnableComponent does not override this method.
     * 
     *  

This method instantiates the skin for the component, * adds the skin as a child of the component, and * resolves all part associations for the skin

* * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ protected function attachSkin():void { // Factory var skinClassFactory:IFactory = getStyle("skinFactory") as IFactory; if (skinClassFactory) setSkin( skinClassFactory.newInstance() as Skin ); // Class if (!skin) { var skinClass:Class = getStyle("skinClass") as Class; if (skinClass) setSkin( new skinClass() ); } if (skin) { skin.owner = this; // As a convenience if someone has declared hostComponent // we assign a reference to ourselves. If the hostComponent // property exists as a direct result of utilizing [HostComponent] // metadata it will be strongly typed. We need to do more work // here and only assign if the type exactly matches our component // type. if ("hostComponent" in skin) { try { Object(skin).hostComponent = this; } catch (err:Error) {} } // the skin's styles should be the same as the components skin.styleName = this; super.addChild(skin); skin.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, skin_propertyChangeHandler); } else { throw(new Error(resourceManager.getString("components", "skinNotFound", [this]))); } findSkinParts(); invalidateSkinState(); }

위 코드에 보면 hostComponent 부분이 보이는데 바로 HostComponent 메타데이터와 관련이 있어 보인다. 아직 정확한 동작방식은 모르겠다.

 

주제를 바꿔서 맨 위 코드에 <s:state>가 존재하는 것을 볼 수 있다. 이것은 상태를 의미하는 것으로 이미 Button 자체에 up, down, over, up 상태가 메타 데이터 형태로 정의 되어 있다. . ButtonBase에 보면 아래와 같은 방식으로 4개의 상태를 나타내는 메타 데이터가 정의되어 있다.

/**
 *  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")]

public class ButtonBase extends SkinnableComponent implements IFocusManagerComponent
{
   ...
}

 

저 상태변화는 SkinnableComponent 클래스에서 알아서 처리하도록 되어 있다. Button의 스킨의 상태에 따른 변화의 동작방식을 이해하기 위해 SkinnableComponent를 분석해야한다. 뭐, 분석까지 아니더라도 SkinnableComponent를 활용하는 방법만 알면 될 것이다. SkinnableComponent는 UIComponent를 확장한 것이고 모든 Spark 계열의 컴포넌트는 SkinnableComponent를 확장한 형태이다. ColorPicker나 DateChooser와 같이 예전 Halo 계열의 컴포넌트는 SkinnableComponent를 확장하지 않았다.

 

<s:filters>는 말그대로 필터를 설정하는 것을 의미한다. 여기서는 DropShadowFilter를 사용했다. Flex 4에서 추가한 새로운 기능중에 각 속성에 state를 지정할 수 있다. DropShadowFilter를 사용할 때도 아래 처럼 속성에 이 기능을 적용했다.  

 

alpha="0.5" alpha.over="0.3"

 

DropShadowFilter를 사용한 부분 중에 alpha값을 위처럼 사용했다. 보통때는 alpha값이 0.5이지만 상태가 over이면 0.3을 사용하겠다는 것을 의미한다. alpha.over="0.3" 부분이 어떻게 ActionScript 로 바뀌는가 살펴보려면 컴파일 옵션에 -keep-generated-actionscript를 해보면 알 수 있다. 그럼 컴파일을 하면 src/generated 폴더가 생성된다. 그 안에 com.jidolstar.skins 패키지로 가보면 MyButtonSkin-generated.as 파일이 있는데 열어보면 alpha.over="0.3" 가 어떻게 바뀌는가 한눈에 알 수 있다. 아래 코드는 MyButtonSkin-generated.as의 일부분이다.

 

    /**
     * @private
     **/
    public function MyButtonSkin()
    {
        super();
        // our style settings

        // properties
        this.filters = [_MyButtonSkin_DropShadowFilter1_i()];
        this.layout = _MyButtonSkin_BasicLayout1_c();
        this.mxmlContent = [_MyButtonSkin_Rect1_c(), _MyButtonSkin_SimpleText1_i()];
        this.currentState = "up";


        // events
		states = [
		  new State ({
		    name: "up",
		    overrides: [
		    ]
		  })
		  ,
		  new State ({
		    name: "down",
		    overrides: [
		      new mx.states.SetProperty().initializeFromObject({
		        target: "_MyButtonSkin_DropShadowFilter1",
		        name: "inner",
		        value: true
		      })
		    ]
		  })
		  ,
		  new State ({
		    name: "over",
		    overrides: [
		      new mx.states.SetProperty().initializeFromObject({
		        target: "_MyButtonSkin_DropShadowFilter1",
		        name: "alpha",
		        value: 0.3
		      })
		      ,
		      new mx.states.SetProperty().initializeFromObject({
		        target: "_MyButtonSkin_DropShadowFilter1",
		        name: "distance",
		        value: 15
		      })
		      ,
		      new mx.states.SetProperty().initializeFromObject({
		        target: "_MyButtonSkin_DropShadowFilter1",
		        name: "blurX",
		        value: 15
		      })
		      ,
		      new mx.states.SetProperty().initializeFromObject({
		        target: "_MyButtonSkin_DropShadowFilter1",
		        name: "blurY",
		        value: 15
		      })
		      ,
		      new mx.states.SetProperty().initializeFromObject({
		        target: "_MyButtonSkin_GradientEntry2",
		        name: "ratio",
		        value: 0.25
		      })
		    ]
		  })
		  ,
		  new State ({
		    name: "disabled",
		    overrides: [
		    ]
		  })
		];
    }

private function _MyButtonSkin_DropShadowFilter1_i() : spark.filters.DropShadowFilter
{
	var temp : spark.filters.DropShadowFilter = new spark.filters.DropShadowFilter();
	temp.quality = 3;
	temp.alpha = 0.5;
	temp.distance = 5;
	_MyButtonSkin_DropShadowFilter1 = temp;
	return temp;
}

 

결국 alpha.over="0.3"는 State 객체를 만들어 각 상태에 따른 속성으로 지정해버리도록 만들어진다. currentState를 "up"으로 설정하고 상태가 바뀔 때마다 등록된 State를 실행하도록 되는 것이다. 이걸 보니깐 원리가 확실히 이해가 된다.

 

다시 돌아가서....

<s:Rect>과 <s:SimpleText>는 mxmlContent=[_MyButtonSkin_Rect1_c(), _MyButtonSkin_SimpleText1_i()] 의 형태로 추가 된다. mxmlContent도 Flex 4부터 추가된 속성인데 생소하기 짝이 없다. ㅎㅎ 좀 알아보기 위해 또 뒤져봤다.

 

//----------------------------------
//  mxmlContent
//----------------------------------

private var mxmlContentChanged:Boolean = false;
private var _mxmlContent:Array;

[ArrayElementType("mx.core.IVisualElement")]

/**
 *  The visual content children for this Group.
 *
 *  

The content items should only be IVisualItem objectss. * An mxmlContent Array should not be shared between multiple * Group containers because visual elements can only live in one container * at a time.

* *

If the content is an Array, do not modify the Array * directly. Use the methods defined by Group class instead.

* * @default null * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function get mxmlContent():Array { if (_mxmlContent) return _mxmlContent.concat(); else return null; } /** * @private */ public function set mxmlContent(value:Array):void { if (createChildrenCalled) { setMXMLContent(value); } else { mxmlContentChanged = true; _mxmlContent = value; // we will validate this in createChildren(); } } /** * @private * Adds the elements in mxmlContent to the Group. * Flex calls this method automatically; you do not call it directly. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ private function setMXMLContent(value:Array):void { var i:int; // if there's old content and it's different than what // we're trying to set it to, then let's remove all the old // elements first. if (_mxmlContent != null && _mxmlContent != value) { for (i = _mxmlContent.length - 1; i >= 0; i--) { elementRemoved(_mxmlContent[i], i); } } _mxmlContent = (value) ? value.concat() : null; // defensive copy if (_mxmlContent != null) { var n:int = _mxmlContent.length; for (i = 0; i < n; i++) { var elt:IVisualElement = _mxmlContent[i]; // A common mistake is to bind the viewport property of an Scroller // to a group that was defined in the MXML file with a different parent if (elt.parent && (elt.parent != this)) throw new Error(resourceManager.getString("components", "mxmlElementNoMultipleParents", [elt])); elementAdded(elt, i); } } }

 

위 코드는 Group 클래스이다. mxmlContent는 Skin 클래스의 부모 클래스인 Group에 구현되어 있는 속성인 것이다. mxmlContent에 값을 설정하면 setMXMLContent() 함수가 호출되어 기존의 MXML 추가된 Element들을 지우고 새로운 Element인 Rect과 SimpleText를 추가한다. 또한 IVisibelElement인 컴포넌트만 자식으로 추가하도록 되어 있고 다른 부모를 가지는 컴포넌트인 경우에 예외처리도 한다. 결국 mxmlContent은 Group에서 사용된 MXML형태의 비주얼 컴포넌트를 Flex 컴파일러가 AS3로 추가해주기 위해 만들어진 것이다.

 

<s:Application> 부분에 버튼 추가한 부분은 어떻게 변해 있을까? 살펴보면 아래 처럼 되어 있다.

private function _sparkskin_Button1_c() : spark.components.Button
{
	var temp : spark.components.Button = new spark.components.Button();
	temp.label = "확인";
	temp.setStyle("skinClass", com.jidolstar.skins.MyButtonSkin);
	if (!temp.document) temp.document = this;
	return temp;
}

skinClass가 원래 Style속성이다. SkinnableComponent를 보면 [Style(name="skinClass", type="Class")]로 정의 되어 있다. 이는 validateSkinChange() 함수에서 붙여주게 된다.

 

/**
 *  @private
 */
private function validateSkinChange():void
{
    // If our new skin Class happens to match our existing skin Class there is no
    // reason to fully unload then reload our skin.  
    var skipReload:Boolean = false;
    
    if (_skin)
    {
        var factory:Object = getStyle("skinFactory");
        var newSkinClass:Class;
        
        if (factory)
            newSkinClass = (factory is ClassFactory) ? ClassFactory(factory).generator : null;
        else
            newSkinClass = getStyle("skinClass");
            
        skipReload = newSkinClass && 
            getQualifiedClassName(newSkinClass) == getQualifiedClassName(_skin);
    }
    
    if (!skipReload)
    {
        if (skin)
            detachSkin();
        attachSkin();
    }
}

 

내가 한가지 흥미를 느꼈던 것은 바로 <s:SimpleText> 부분이였다. Flex 4의 Button은 Label의 컴포넌트를 바꿀 수 없었는데 Flex 4에서는 스킨 기능을 이용해 Text엔진을 바꿀 수 있다. 가령 <s:SimpleText> 대신 <s:RichText>로 바꿔보길 바란다. 잘 된다. 이는 TextGraphicElement를 확장한 SimpleText와 RichText만 가능하다. 만약 TextArea와 같은 것을 사용하면 아래와 같은 에러가 나오게 된다.

 

TypeError: Error #1034: 유형 강제 변환에 실패했습니다. spark.components::TextArea@70c36d1을(를) spark.primitives.supportClasses.TextGraphicElement(으)로 변환할 수 없습니다.

 

이런 식의 사용은 Spark 컴포넌트 전체에서 사용할 수 있다. 참 좋은 방법이다.

 

 

생각하기

 

Flex 4는 Flex 3 처럼 무리하게 ActionScript 3.0까지 분석할 필요까지 느끼게 해주지 않도록 하는 것이 컨셉인 것 같다. 하지만 내가 좀 독특한 건가 예전 버릇을 버리지 못해 어찌 돌아가는지 알고 싶어서 Flex 4 SDK를 훑어보게 되었다. 하지만 이렇게 저렇게 분석하면서 살펴봤지만 아직도 모르는게 산적해있다. 먼저 사용하는 법부터 좀 공부해야겠다. ㅎㅎ

 

관련글

Getting started with Spark skins

Creating Spark Skins

The Spark Group and Spark SkinnableContainer containers

 

 

글쓴이 : 지돌스타(http://blog.jidolstar.com/558)

 

+ Recent posts