Flex/AIR 개발을 하다보면 개발자가 정의하는 커스텀 컴포넌트를 제작할 필요가 있을 때가 많다. 기존 Flex 컴포넌트를 조합하는 경우도 있는 반면, UIComponent를 확장해서 기존에 있지 않던 컴포넌트를 만드는 경우도 종종 있다.

이 글은 Flex 또는 AIR 커스텀 컴포넌트를 만드는데 있어서 한가지 Tip을 언급한다.
아래 프로그램 처럼 Slider를 만들었다고 하자. 이 Slider는 매우 단순하다. 단순히 Button객체만 자식으로 등록되어 있고 마우스를 이용해 버튼을 좌우측으로 이동할 수 있다.

 


여기서 중요하게 다루는 것은 바로 Thumb을 마우스로 잡고 움직일 때이다.

 

위 프로그램에서 Thumb부분을 마우스로 잡아 움직여 보자.  마우스를 누른 상태에서 Thumb의 영역을 벗어나 상단이나 하단에서 움직여보자. 또 빠르게 움직여보자. 마우스 위치가 Thumb위에 있지 않은데도 Thumb은 잘 움직인다. 마우스 버튼을 누르지 않는 이상(Mouse Up) Thumb은 정상적으로 잘 움직인다. 당연히 그래야하지만 개발자에게 당연한 것은 당연하지 않을 수 있다. “이것을 어떻게 구현했을까?” 한번 고민하게 된다는 것이다.

 

매우 단순한 컴포넌트임에도 불구하고 아래 내용을 알지 못하면 이런 컴포넌트를 만드는데 삽질할 수 있다.

 

1. Shields를 사용하지 않기

Shield란 무엇인가? 방패, 보호물 뜻을 가지고 있다. 방금 보여준 Flex 프로그램에서 Thumb을 마우스로 잡고 움직일 때 Thumb의 범위를 벗어나도 잘 움직였던 것은 Shield가 있기 때문이다. Shield가 때문에 Thumb의 움직임이 보호받는 것이라 생각하면 되겠다.

 

어떻게 구현 했을까 단순하게 생각한다면 Thumb 주변에 안보이는 Shield가 존재해서 Thumb의 범위를 연장했다고 보면 된다. Thumb의 범위가 연장되면 그 영역의 마우스 이벤트를 받을 수 있기 때문에 위에서 보여준 프로그램과 같이 구현할 수 있을 것이다.

위 프로그램의 소스를 살펴보겠다.

 

 

위 소스를 다운로드 받아 실행해봐도 되겠다. Flex SDK 3.2 환경이다.


<?xml version=“1.0″ encoding=“utf-8″?>
<mx:Application
    xmlns:mx=“http://www.adobe.com/2006/mxml”
    xmlns:local=“*”
    layout=“vertical”>
    <local:MySlider width=“200″ height=“5″/>
    <mx:Label text=“jidolstar.com”/>
</mx:Application>

 

Flex Application 은 매우 단순하다.  MySlider 컴포넌트를 자식으로 추가했을 뿐이다. 그럼 MySlider를 살펴보자.


package
{
    import flash.display.DisplayObject;
    import flash.events.Event;
    import flash.events.MouseEvent;
   
    import mx.controls.Button;
    import mx.core.UIComponent;
    import mx.events.ResizeEvent;
    import mx.events.SandboxMouseEvent;

    /**
     * 슬라이더 예제
     * systemManager.deployMouseShields() 함수를 활용하는 예제이다.
     * @author Yongho Ji
     * @since 2009.02.11
     */
    public class MySlider extends UIComponent
    {
        /**
         * @private
         * thumb으로 이용할 버튼
         */
        private var thumb:Button;
       
        /**
         * @private
         * thumb의 위치
         */
        private var thumbPos:Number = 0;
       
        /**
         * @private
         * thumb의 한계 위치
         */
        private var maxThumbPos:Number;
       
        /**
         * @private
         * thumb 위치가 바뀌었을 때 true 설정
         */
        private var thumbPosChanged:Boolean = true;
       
        /**
         * @private
         * Track 그리기를 요청할때 true 설정
         */
        private var drawTrackRequest:Boolean = true;
       
        /**
         * 생성자
         */
        public function MySlider()
        {
            super();
            this.addEventListener( ResizeEvent.RESIZE, resizeHandler );
        }
       
     /**
     * @private
     */
        protected override function createChildren():void
        {
            super.createChildren();
           
            //thumb 생성
            if( !thumb )
            {
                thumb = new Button();
                thumb.addEventListener(MouseEvent.MOUSE_OVER,
                                        thumb_mouseOverHandler );
                thumb.setActualSize(20,20);
                addChild( thumb );    
            }
        }

     /**
     * @private
     */
        protected override function measure():void
        {
            super.measure();
           
            //기본 사이즈 설정
            this.minWidth = 100;
            this.minHeight = 3;
        }

     /**
     * @private
     */
        protected override function updateDisplayList(uw:Number, uh:Number):void
        {
            super.updateDisplayList( uw, uh );
           
            //Track그리기
            if( drawTrackRequest )
            {
                //최대 위치값 조절
                maxThumbPos = uw - thumb.width;
                setThumbPos( thumbPos );
                drawTrackRequest = false;
                graphics.clear();
                graphics.beginFill( 0×000000, 1 )
                graphics.drawRect(0,0,uw,uh);
                graphics.endFill();
            }
           
            //Thumb위치조절
            if( thumbPosChanged )
            {
                thumbPosChanged = false;
                thumb.move( thumbPos, uh/2-thumb.height/2 );
            }
        }
       
        /**
         * @private
         * thumb의 위치를 지정한다.
         */
        private function setThumbPos( pos:Number ):void
        {
            if( thumbPos == pos ) return;
           
            thumbPos = pos-thumb.width/2;
            if( thumbPos < 0 )
            {
                thumbPos = 0;
            }
            else if( thumbPos > maxThumbPos )
            {
                thumbPos = maxThumbPos;
            }
            this.thumbPosChanged = true;
            this.invalidateDisplayList();
        }
       
        /**
         * @private
         * track을 다시 그리도록 요청
         */
        private function resizeHandler(event:ResizeEvent ):void
        {
            drawTrackRequest = true;
            this.invalidateDisplayList();
        }

        /**
         * @private
         */
        private function thumb_mouseOverHandler( event:MouseEvent ):void
        {
            thumb.addEventListener( MouseEvent.MOUSE_OUT, thumb_mouseOutHandler );
            thumb.addEventListener( MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler );
        }
       
        /**
         * @private
         */
        private function thumb_mouseOutHandler( event:MouseEvent ):void
        {
            thumb.removeEventListener( MouseEvent.MOUSE_OUT, thumb_mouseOutHandler );
            thumb.removeEventListener( MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler );
        }        
       
        /**
         * @private
         */
        private function thumb_mouseUpHandler( event:MouseEvent ):void
        {
            thumb_mouseLeaveHandler(event);
        }

        /**
         * @private
         */
        private function thumb_mouseMoveHandler( event:MouseEvent ):void
        {
            if( event.buttonDown == false )
            {
                thumb_mouseLeaveHandler(event);
                return;
            }            
            setThumbPos( mouseX );
        }
       
        /**
         * @private
         */
        CONFIG::MOUSE_SHIELDS
        private function thumb_mouseDownHandler( event:MouseEvent ):void
        {
     var sbRoot:DisplayObject = systemManager.getSandboxRoot();
     sbRoot.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler, true);
     sbRoot.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler, true);
     sbRoot.addEventListener( SandboxMouseEvent.MOUSE_UP_SOMEWHERE, thumb_mouseLeaveHandler); // in case we go offscreen
     systemManager.deployMouseShields(true);    
     setThumbPos( mouseX );                
        }
               
        /**
         * @private
         */
        CONFIG::MOUSE_SHIELDS
        private function thumb_mouseLeaveHandler(event:Event):void
        {
            var sbRoot:DisplayObject = systemManager.getSandboxRoot();
            sbRoot.removeEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler, true);
            sbRoot.removeEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler, true);
            sbRoot.removeEventListener( SandboxMouseEvent.MOUSE_UP_SOMEWHERE, thumb_mouseLeaveHandler); // in case we go offscreen
            systemManager.deployMouseShields(false);            
        }
       
        /**
         * @private
         */
        CONFIG::THUMB_SHIELDS
        private function thumb_mouseDownHandler( event:MouseEvent ):void
        {
            thumb.graphics.clear();
            thumb.graphics.beginFill( 0xff0000, 0.4 );
            thumb.graphics.drawRect( -100, -100, thumb.width+200, thumb.height+200 );
            thumb.graphics.endFill();
            thumb.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
            setThumbPos( mouseX );                
        }
               
        /**
         * @private
         */
        CONFIG::THUMB_SHIELDS
        private function thumb_mouseLeaveHandler(event:Event):void
        {
            thumb.graphics.clear();        
            thumb.removeEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.removeEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
        }    
       
        /**
         * @private
         */
        CONFIG::NONE_SHIELDS
        private function thumb_mouseDownHandler( event:MouseEvent ):void
        {
            thumb.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
     setThumbPos( mouseX );                
        }
               
        /**
         * @private
         */
        CONFIG::NONE_SHIELDS
        private function thumb_mouseLeaveHandler(event:Event):void
        {
            thumb.removeEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler );
            thumb.removeEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler );
        }                
    }
}

 

Flex로 컴포넌트를 제작해보신 분들이라면 위 코드를 어렵지 않게 해석할 수 있으리라 판단한다.

 

이벤트 핸들러에 “CONFIG::~” 가 있는데 이것은 조건부 컴파일을 위해 존재한다. 이 예제는 3가지 경우에 따라 컴파일을 해서 테스트 하기 때문에 이 부분을 추가했다. 가령 CONFIG::NONE_SHIELDS가 있는 함수는 컴파일 하되 CONFIG::THUMB_SHIELD, CONFIG::MOUSE_SHIELD 가 붙은 함수는 컴파일에 제외하려면 컴파일 옵션에 다음을 추가하면 된다. C언어에서 #if, #endif와 비슷한 것이라 생각하면 되겠다.

-define=CONFIG::MOUSE_SHIELDS,false
-define=CONFIG::THUMB_SHIELDS,false
-define=CONFIG::NONE_SHIELDS,true

여기서는 Shield를 사용하지 않는 예이므로 위처럼 컴파일 옵션을 지정하면 되겠다. 프로그램을 실행해 보면 아래와 같다.

 

 

처음에 보여준 것과 겉모습으로는 차이가 없다.  하지만 Thumb을 움직여보면 바로 어떤 문제가 있는지 확인할 수 있을 것이다. Thumb을 잡고 움직이되 빨리 움직여보자. 제대로 움직이는가? 마우스 커서가 Thumb을 벗어나면 더 이상 움직이지 않는다. 왜 그럴까? 바로 Thumb이 받을 수 있는 마우스 이벤트 영역은 Thumb크기에 제한되어 있기 때문이다. Thumb만 벗어나면 MouseOut 이벤트가 발생해서 더 이상 MouseMove에 대응하는 움직임을 수행할 수 없는 것이다.

 

이것이 바로 Shield를 만들지 않고 사용한 결과이다.

 

이렇게 컴포넌트를 만들면 당연히 혼난다. ^^;;

 

2. Thumb에 Shield를 만들기

첫번째 예제에서 Shield의 필요성을 느꼈을 것이다. 그래서 이번에는 Thumb을 움직이기 위해 MouseDown했을 때, Thumb보다 어느 정도 크게 Shield를 만들어 Thumb을 약간 벗어난다 하더라도 지속적으로 Thumb을 움직일 수 있게 해보겠다.

이번에는 아래처럼 컴파일 옵션을 줘보자.

-define=CONFIG::MOUSE_SHIELDS,false
-define=CONFIG::THUMB_SHIELDS,true
-define=CONFIG::NONE_SHIELDS,false

아래는 컴파일한 결과물을 실행한 모습이다.

 

 

위 프로그램에서 Thumb를 움직여보자. 빨간색으로 나오는 것은 Thumb에 그려준 Shield이다. 이 영역이 Thumb을 벗어나도 계속 Thumb을 움직일 수 있게 마우스 감지 영역을 잡는 역할을 해준다. 필요하다면 더 크게 그려도 되며 실제 만들때는 alpha속성을 0으로 주어 보이지 않게 해야할 것이다.

 

Shield영역을 벗어나지 않는 이상 잘 움직인다. 이제 Shield영역만 좀 크게 잡아주면 문제가 없어보인다.

 

하지만….

문제는 아직도 있다. Thumb을 움직이면서 아래 “jidolstar.com”  Label위로 올려보자. 더이상 Thumb이 움직이지 않는다. 이 문제는 MySlider와 Label의 객체가 그의 부모 위에 붙은 순서가 MySlider-Label순이기 때문이다. 만약 Label-MySlider 순으로 붙었다면 아무 상관없을 것이지만 그것을 어찌 알 수 있겠는가? 만약 이대로 MySlider를 만들어 배포하면 다른 컴포넌트들과 함께 붙여 사용할 것인데 위와 같은 문제를 발생시키지 않으려면 MySlider가 가장 높은 위치에 있어야 할 것이다. 그러므로 사용하기에는 여전히 문제가 많다!!!

 

아마도 Flex SDK를 분석해보지 않은 사람이라면 위와 같이 프로그래밍을 할 가능성이 크다. 본인도 그랬으니깐… ^^;;

 

3. SystemManager.deployMouseShields()를 이용한 Shield를 만들어 사용하기

이 부분이 첫번째, 두번째 예제에서 보여준 문제점을 깔끔하게 해결하는 방법이다. 먼저 아래와 같이 컴파일 옵션을 설정하자.

-define=CONFIG::MOUSE_SHIELDS,true
-define=CONFIG::THUMB_SHIELDS,false
-define=CONFIG::NONE_SHIELDS,false

실행해보면 아주 잘된다!

 

 

이쯤되면 “이거 어떻게 한거야?” 궁금할것이다.

 

본인은 첫번째, 두번째 예제와 같은 문제점을 겪으면서 해결방법이 없을까 생각하다가 Flex SDK에 있는 Scrollbar 컴포넌트에서 답을 얻었다. 이 문제점은 검색을 해도 해결방법이 안나온다. Flex SDK를 만든 사람과 경험해 본 사람만 알 수 있다.

 

위 코드에서 CONFIG::MOUSE_SHIELDS 를 부분이 있는 thumb_mouseDownHandler()와 thumb_mouseLeaveHandler()를 보자. 아래처럼 낯선 코드가 있다.

var sbRoot:DisplayObject = systemManager.getSandboxRoot();

sbRoot.addEventListener( MouseEvent.MOUSE_UP, thumb_mouseUpHandler, true);

sbRoot.addEventListener( MouseEvent.MOUSE_MOVE, thumb_mouseMoveHandler, true);

sbRoot.addEventListener( SandboxMouseEvent.MOUSE_UP_SOMEWHERE, thumb_mouseLeaveHandler); // in case we go offscreen

systemManager.deployMouseShields(true);

Flex는 SystemManager가 모든 운영의 중심에 있다. Application이 있다고 생각하겠지만 사실은 아니다. SystemManager가 Application을 생성하고 그외 중요한 모든 핵심기능을 가지고 있다. Application은 단지 컴포넌트를 담는 그릇 정도라고 생각하면 된다. 이 SystemManager를 잘 활용하면 해결하기 어려운 것도 해결하기 쉬워질 수 있다.

 

위 코드처럼 모든 UIComponent를 상속한 컴포넌트는 systemManager를 참조할 수 있다. 여기서 사용한 코드는 SystemManager에 정의된 getSandboxRoot()와 deployMouseShields() 함수이다. getSandboxRoot()는 샌드박스내에 최상위 systemManager를 취득하는데 쓰인다. 즉 지금 보고 있는 애플리케이션의 root이다. 그리고 deployMouseShields()는 Mouse 이벤트를 탐지할 Shield를 만들어주는 역할을 한다. 이 Shield가 getSandboxRoot()로 부터 얻은 systemManager에 등록된 마우스 이벤트 핸들러가 동작할 수 있는 마우스 이벤트 감지 영역인 것이다. addEventListener로 이벤트를 등록시 3번째 인자인 caputure에 true를 붙인 이유는 bubble과정에서 이벤트를 감지는 자식들을 통과해 systemManager로 올라오기 때문에 무의미하므로 바로 systemManager에서 마우스 이벤트를 첫번째로 받기 위함이다.

정리하며

Flex SDK에 있는 기존 컴포넌트가 어떻게 만들어졌는가 분석하는 것은 커스텀 컴포넌트를 제대로 제작하는 가이드 라인이 될 수 있다. 이러한 부분들이 모두 Adobe Livedocs에 잘 설명되어 있으면 좋겠지만 그렇지 않은 부분도 상당히 많다. 그 부분은 개발자들이 알아서 찾아내야한다. Flex/AIR 개발을 하시는 분이라면 어떤 부분을 구현하기 위해 너무 자신의 생각으로 개발하려하지 말자. 대신 Flex SDK에 있는 컴포넌트에서 그 구현을 할 법만한 부분을 찾아 분석한 후 적용하는 편이 훨씬 좋은 코드를 만들 수 있다. 한마디로 Flex SDK를 까보라는 말이다. ^^

관련 사이트

+ Recent posts