Tutorial - Creating a reusable Slider Control / Component

Hi Everyone,

Today I will cover the subject of creating a reusable slider control which takes two bitmaps as resources. The thumb bitmap will slide on the rail bitmap. It is a very simple concept!

We will basically take this image:
and this one:
and make this (try it, move that knob):


In fact, the code you are about to write will take ANY two images and create a slider with them. Just like magic, as long as your images make some sense ( a track and a button ), it should be golden!

Starting Point
When I started writing this tutorial, I was still using the Flash IDE; but since then I am free!! I now use exclusively FlashDevelop. With that in mind, the package to start the coding, if you want to follow along is here. You will need FlashDevelop & the Flex SDK 3.4 or newer.

In the assets directory, I put the two bitmaps:

  • bar.png -- which contains the background
  • button.png -- which contains the foreground

Then I create a small class, which I called GraphicAssets like so:

Actionscript:
  1. package assets
  2. {
  3.     import flash.display.Bitmap;
  4.     /**
  5.      * ...
  6.      * @author Martin Legris
  7.      */
  8.  
  9.     public class GraphicAssets
  10.     {
  11.         [Embed(source="../../assets/bar.png")]
  12.         public static const BAR:Class;
  13.        
  14.         [Embed(source="../../assets/button.png")]
  15.         public static const BUTTON:Class;
  16.        
  17.         public static function getBitmap(template:Class):Bitmap
  18.         {
  19.             var inst:Object = new template();
  20.             var bitmap:Bitmap = new Bitmap(inst.bitmapData);
  21.             return bitmap;
  22.         }
  23.     }
  24. }

Notice the function getBitmap() which returns a bitmap, it is simply because I don't like overhead, and if you instantiate either GraphicAssets.BAR or GraphicAssets.BUTTON directly you end up with a BitmapAsset, and well, we don't need that.

To use it we'd simply do something like this:

Actionscript:
  1. var button:Bitmap = GraphicAssets.getBitmap(GraphicAssets.BUTTON);
  2. // or
  3. var bar:Bitmap = GraphicAssets.getBitmap(GraphicAssets.BAR);

Startup Properties
I sometimes create a class which contains the modifiable properties of a Component. In this case, I called it SpriteSliderProperties and here is the code:

Actionscript:
  1. package newcommerce.controls
  2. {
  3.     import flash.display.DisplayObject;
  4.     import flash.display.Sprite;
  5.     import flash.events.EventDispatcher;
  6.    
  7.     /**
  8.      * ...
  9.      * @author Martin Legris ( http://blog.martinlegris.com )
  10.      */
  11.    
  12.     public class SpriteSliderProperties
  13.     {
  14.         protected var _bg:DisplayObject;
  15.         protected var _fg:DisplayObject;
  16.        
  17.         protected var _min:Number;
  18.         protected var _max:Number;
  19.         protected var _stepSize:Number;
  20.         protected var _decimals:Number;
  21.         protected var _defaultValue:Number;
  22.        
  23.         public function get fg():DisplayObject { return _fg; }
  24.         public function set fg(value:DisplayObject):void { _fg = value; }
  25.        
  26.         public function get bg():DisplayObject { return _bg; }
  27.         public function set bg(value:DisplayObject):void { _bg = value; }
  28.        
  29.         public function get min():Number { return _min; }
  30.         public function set min(value:Number):void { _min = value; }
  31.        
  32.         public function get max():Number { return _max; }
  33.         public function set max(value:Number):void { _max = value; }
  34.        
  35.         public function get stepSize():Number { return _stepSize; }
  36.         public function set stepSize(value:Number):void { _stepSize = value; }
  37.        
  38.         public function get decimals():Number { return _decimals; }
  39.         public function set decimals(value:Number):void { _decimals = value; }
  40.        
  41.         public function get defaultValue():Number { return _defaultValue; }
  42.         public function set defaultValue(value:Number):void { _defaultValue = value; }
  43.        
  44.         public function SpriteSliderProperties(bg:DisplayObject, fg:DisplayObject, min:Number = 0, max:Number = 100, stepSize:Number = 10, decimals:Number = 0, defaultValue:Number = NaN)
  45.         {
  46.             _bg = bg;
  47.             _fg = fg;
  48.            
  49.             _min = min;
  50.             _max = max;
  51.             _stepSize = stepSize;
  52.            
  53.             _decimals = decimals;
  54.            
  55.             if (isNaN(defaultValue))
  56.                 _defaultValue = min;
  57.             else
  58.                 _defaultValue = defaultValue;
  59.         }
  60.     }
  61. }

As described in the code, we have one DisplayObject which will act as the background of the component, and another that will act as the foreground (movable) object in the component. There is also:

  • A minimum value, defaulted at 0
  • A maximum value, defaulted at 100 -- basically this means the slider will have values ranging from 0 to 100
  • The step size, when clicking besides the button itself. This is not going to be implemented, let's call it your homework!
  • The amount of numbers after the decimal that we want
  • The default value, or starting value; value demonstrated before the user interacts with it

Our usage scenario.. the target!
What we want is to create this slider but passing a SpriteSliderProperties object to the constructor; then add it to the stage. Then position it if we'd like and finally listen to events thrown by the Slider instance. The idea is to have two types of events:

  • ValueEvent.PREVIEW -- called while the user is moving the slider button
  • ValueEvent.CHANGE -- called once the user is done moving the button

Here is sample code, our target:

Actionscript:
  1. var bg:Bitmap = GraphicAssets.getBitmap(GraphicAssets.BAR);
  2. var fg:Bitmap = GraphicAssets.getBitmap(GraphicAssets.BUTTON);
  3.  
  4. var props:SpriteSliderProperties = new SpriteSliderProperties(bg, fg, 0, 100, 10, 1, 0);
  5. var slider:SpriteSlider = new SpriteSlider(props);
  6. addChild(_slider);
  7.  
  8. slider.addEventListener(ValueEvent.PREVIEW, doSliderValuePreview);
  9. slider.addEventListener(ValueEvent.CHANGED, doSliderValueChanged);
  10.  
  11. slider.x = 0;
  12. slider.y = (stage.stageHeight - _slider.height) / 2;

ValueEvent class
This class carries the value inside of it. Here is the source code:

Actionscript:
  1. package newcommerce.events
  2. {
  3.     import flash.events.Event;
  4.    
  5.     /**
  6.      * ...
  7.      * @author Martin Legris
  8.      */
  9.     public class ValueEvent extends Event
  10.     {
  11.         public static const CHANGED:String = "value_changed_event";
  12.         public static const PREVIEW:String = "value_preview_event";
  13.        
  14.         protected var _value:Number = 0;
  15.         public function get value():Number { return _value; }      
  16.        
  17.         public function ValueEvent(type:String, value:Number)
  18.         {
  19.             _value = value;
  20.             super(type);
  21.         }
  22.     }
  23. }

Nothing complicated there.

The meat
Now that the context is well established, lets get down to business. I will follow the methodology I started describing in this post (I promise to finish it sometime in the future). Just to recap.. At creation time the following functions are called:

  • init() -- initialization of member variables like arrays, dictionaries, timers ...
  • createChilds() -- the creation of displayObject used inside of the component
  • initEvents() -- adding event listeners
  • arrange() -- positioning the childs
  • redraw() -- drawing of the interface, if needed

Then, when the function setSize() is called:

  • arrange()
  • redraw()

In the component model of Flex there is the concept of invalidation; where you invalidate certain properties of the components and the code that will react to this invalidation is executed using callLater (on the next Frame). This way if you modify the size, then maybe data, arrange() (for example) will only be executed once, not twice. I showed how to implement your own callLater function in this post. For this example, I won't go there. There is no need...

In this example, I use newcommerce.controls.BaseControl as the parent class to SpriteSlider. This class contains the following code (you don't need to read it all if you don't want to). This class is contained in the starter packaged you might have downloaded at the beginning of this tutorial.

BaseControl

Actionscript:
  1. package newcommerce.controls{
  2.    
  3.     import flash.display.DisplayObject;
  4.     import flash.display.Sprite;
  5.     import flash.events.MouseEvent;
  6.     import flash.events.Event;
  7.     import ascb.drawing.Pen;
  8.     import newcommerce.drawing.AbstractDrawing;
  9.    
  10.     public class BaseControl extends Sprite{
  11.    
  12.         public var _width:Number = 0;
  13.         public var _height:Number = 0;
  14.         private var _padding:Number = 5;
  15.         protected var _mouseDown:Boolean = false;
  16.         protected var _drawing:AbstractDrawing;
  17.        
  18.         public override function get width():Number
  19.         {
  20.             return _width;
  21.         }
  22.        
  23.         public override function get height():Number
  24.         {
  25.             return _height;
  26.         }
  27.        
  28.         public function get drawing():AbstractDrawing
  29.         {
  30.             return _drawing;
  31.         }
  32.        
  33.         public function set drawing(drawing:AbstractDrawing):void
  34.         {
  35.             _drawing = drawing;
  36.         }
  37.        
  38.         public function get padding():Number
  39.         {
  40.             return _padding;
  41.         }
  42.        
  43.         public function set padding(padding:Number):void
  44.         {
  45.             _padding = padding;
  46.         }
  47.       
  48.       // The setSize() method is automatically called when the authoring time instance is resized.
  49.         public function setSize(nW:Number, nH:Number):void {
  50.             scaleX = 1;
  51.             scaleY = 1;
  52.             _width = nW;
  53.             _height = nH;
  54.             arrange();
  55.             redraw();
  56.         }
  57.        
  58.         public function getSize():Object
  59.         {
  60.             return {width:_width, height:_height};
  61.         }
  62.        
  63.  
  64.         public function BaseControl(initObj:Object)
  65.         {         
  66.             for(var i:String in initObj)
  67.             {
  68.                 this[i] = initObj[i];
  69.             }
  70.            
  71.             init();
  72.             createChilds();
  73.             initEvents();
  74.             arrange()
  75.             redraw();
  76.         }      
  77.        
  78.         protected function init():void
  79.         {
  80.             if(_width <= 0)   
  81.                 _width = width;
  82.             if(_height <= 0)
  83.                 _height = height;
  84.                
  85.             scaleX = 1;
  86.             scaleY = 1;
  87.         }
  88.        
  89.         protected function initEvents():void
  90.         {
  91.             addEventListener(MouseEvent.MOUSE_DOWN, doMouseDown);
  92.             addEventListener(MouseEvent.MOUSE_UP, doMouseUp);
  93.             addEventListener(Event.ADDED_TO_STAGE, doAddedToStage);
  94.         }
  95.        
  96.         protected function createChilds():void
  97.         {         
  98.         }
  99.        
  100.         protected function doAddedToStage(evt:Event):void
  101.         {
  102.             stage.addEventListener(MouseEvent.MOUSE_UP, doParentRelease);
  103.         }
  104.        
  105.         public function triggerRedraw():void
  106.         {
  107.             redraw();
  108.         }
  109.        
  110.         public function rearrange():void
  111.         {
  112.             arrange();     
  113.         }      
  114.        
  115.         protected function redraw():void
  116.         {
  117.             var pen:Pen = new Pen(graphics);
  118.             pen.clear();
  119.             pen.lineStyle(0,0,0);
  120.             pen.moveTo(0,0);
  121.             pen.beginFill(0,0);
  122.             pen.drawRect(0,0,_width,_height);
  123.             pen.endFill();
  124.         }
  125.        
  126.         protected function arrange():void
  127.         {
  128.             x = Math.round(x);
  129.             y = Math.round(y);
  130.         }
  131.        
  132.         protected function doMouseDown(evt:MouseEvent):void
  133.         {
  134.             _mouseDown = true;
  135.         }
  136.        
  137.         protected function doMouseUp(evt:MouseEvent):Boolean
  138.         {
  139.             if(_mouseDown)
  140.             {
  141.                 _mouseDown = false;
  142.                 return true;
  143.             }
  144.             else
  145.             {
  146.                 return false;
  147.             }
  148.         }
  149.        
  150.         protected function initBtn(btn:*):void
  151.         {
  152.             btn.useHandCursor = true;
  153.             btn.buttonMode = true;
  154.             btn.mouseChildren = false;
  155.         }
  156.        
  157.         public function doParentRelease(evt:MouseEvent):void
  158.         {
  159.             for(var i:Number = 0; i <this.numChildren; i++)
  160.             {
  161.                 var child:DisplayObject = getChildAt(i);
  162.                 if(child is BaseControl)
  163.                     (child as BaseControl).doParentRelease(evt);
  164.             }
  165.            
  166.             doMouseUp(evt);
  167.         }
  168.        
  169.         public function mouseInside():Boolean
  170.         {
  171.             return (mouseX>= 0 && mouseX <= _width && mouseY>= 0 && mouseY <= _height);
  172.         }
  173.     }
  174. }

Basically it has some functionality like:

  • making sure the control is on a whole pixel (not x:4.51 and y:9.56 for example)
  • takes care of covering the components space with a transparent layer to make sure we capture mouse event properly
  • ensures the component is never scaled directly
  • takes over the reporting of .width and .height
  • and has a few helper functions

Most of the functions below override the functions coded in BaseControl, and it is important to call the original in most cases by using super.init(); for example.

Initialization Code
The method I use for drag & drop uses a Timer object. Since this object is not part of the display list, I initialize it in the init() function; then set the slider's value to the defaultValue.

Actionscript:
  1. override protected function init():void
  2. {
  3.     super.init();
  4.     _timer = new Timer(33, 0);
  5.     _timer.addEventListener(TimerEvent.TIMER, doTimer);
  6.            
  7.     _value = _props.defaultValue;
  8. }

Creating Children Objects
Because we will listen to mouse events on the button & track, they both need to be Sprite objects. Hence we need to make sure the DisplayObjects passed in the Properties class ARE sprites, or if not, put them inside of sprites and use those as BG & FG. Hence this function which does exactly that.

Actionscript:
  1. protected function spritify(obj:DisplayObject):Sprite
  2. {
  3.     if (!(obj is Sprite))
  4.     {
  5.         var sprt:Sprite =  new Sprite();
  6.         sprt.addChild(obj);
  7.         obj = sprt;
  8.     }
  9.    
  10.     return obj as Sprite;
  11. }

Now, we will spritify both _props.bg & _props.fg, name them, add as childs of SpriteSlider and finally make sure we display the correct cursor when the mouse is over them. Like so:

Actionscript:
  1. override protected function createChilds():void
  2. {
  3.     super.createChilds();
  4.    
  5.     _bg = spritify(_props.bg);
  6.     _bg.name = "bg";           
  7.     addChild(_bg);
  8.     initBtn(_bg);
  9.    
  10.     _fg = spritify(_props.fg);
  11.     _fg.name = "fg";
  12.     addChild(_fg);
  13.     initBtn(_fg);
  14.    
  15.     _width = _bg.width;
  16.     _height = Math.max(_fg.height, _bg.height);
  17. }

You see at the end I take the width of the _bg and make it the width if the Component, and take the biggest of heights (from _fg and _bg) as the component's height.

Here is the full code, I will continue later...

Actionscript:
  1. package newcommerce.controls
  2. {
  3.     import flash.display.DisplayObject;
  4.     import newcommerce.events.ValueEvent;
  5.     import flash.display.Sprite;
  6.     import flash.events.MouseEvent;
  7.     import flash.events.TimerEvent;
  8.     import flash.utils.Timer;
  9.    
  10.     /**
  11.      * @eventType newcommerce.events.ValueEvent.CHANGED
  12.      */
  13.     [Event(name="value_changed_event",type="newcommerce.events.ValueEvent")]
  14.  
  15.     /**
  16.      * @eventType newcommerce.events.ValueEvent.PREVIEW
  17.      *
  18.      */
  19.     [Event(name="value_preview_event",type="newcommerce.events.ValueEvent")]
  20.    
  21.     /**
  22.      * ...
  23.      * @author Martin Legris -- http://blog.martinlegris.com
  24.      */
  25.     public class SpriteSlider extends BaseControl
  26.     {
  27.         protected var _bg:Sprite;
  28.         protected var _fg:Sprite;
  29.        
  30.         protected var _value:Number;
  31.         protected var _pos:Number;
  32.         protected var _lastValue:Number;
  33.        
  34.         protected var _timer:Timer;
  35.        
  36.         protected var _props:SpriteSliderProperties;
  37.        
  38.         public function get value():Number { return _value; }
  39.         public function set value(value:Number):void { _value = value; updatePosFromValue() }
  40.        
  41.         override public function get width():Number { return super.width; }  
  42.         override public function set width(value:Number):void { throw new Error("Cannot set the width manually on a SpriteSlider"); }
  43.        
  44.         override public function get height():Number { return super.height; }
  45.         override public function set height(value:Number):void { throw new Error("Connot set the heigth manually on a SpriteSlider"); }
  46.        
  47.         protected function get minPos():Number { return 0 + _fg.width / 2; }       
  48.         protected function get maxPos():Number { return _width - _fg.width / 2; }
  49.         protected function get span():Number { return max - min; }
  50.         protected function get space():Number { return _bg.width - _fg.width; }
  51.        
  52.         protected function get min():Number { return _props.min; }
  53.         protected function get max():Number { return _props.max; }
  54.         protected function get stepSize():Number { return _props.stepSize; }
  55.        
  56.         public function get props():SpriteSliderProperties { return _props; }      
  57.        
  58.         public function SpriteSlider(props:SpriteSliderProperties)
  59.         {
  60.             _props = props;
  61.             super({});
  62.         }
  63.        
  64.         override protected function init():void
  65.         {
  66.             super.init();
  67.             _timer = new Timer(33, 0);
  68.             _timer.addEventListener(TimerEvent.TIMER, doTimer);
  69.            
  70.             _value = _props.defaultValue;
  71.         }
  72.        
  73.         override protected function createChilds():void
  74.         {
  75.             super.createChilds();
  76.            
  77.             _bg = spritify(_props.bg);
  78.             _bg.name = "bg";           
  79.             addChild(_bg);
  80.             initBtn(_bg);
  81.            
  82.             _fg = spritify(_props.fg);
  83.             _fg.name = "fg";
  84.             addChild(_fg);
  85.             initBtn(_fg);
  86.            
  87.             _width = _bg.width;
  88.             _height = Math.max(_fg.height, _bg.height);
  89.         }
  90.        
  91.         protected function spritify(obj:DisplayObject):Sprite
  92.         {
  93.             if (!(obj is Sprite))
  94.             {
  95.                 var sprt:Sprite =  new Sprite();
  96.                 sprt.addChild(obj);
  97.                 obj = sprt;
  98.             }
  99.            
  100.             return obj as Sprite;
  101.         }
  102.  
  103.         override protected function initEvents():void
  104.         {
  105.             super.initEvents();
  106.             _fg.addEventListener(MouseEvent.MOUSE_DOWN, doBtnDown);
  107.             _bg.addEventListener(MouseEvent.MOUSE_DOWN, doBtnDown);
  108.         }
  109.  
  110.         protected function doTimer(evt:TimerEvent):void
  111.         {
  112.             _pos = mouseX;
  113.             _pos = Math.max(_pos, minPos);
  114.             _pos = Math.min(_pos, maxPos);
  115.            
  116.             setValue((_pos - minPos) / space * span + min);
  117.            
  118.             if(_value != _lastValue)
  119.                 dispatchEvent(new ValueEvent(ValueEvent.PREVIEW, _value));
  120.            
  121.             _lastValue = _value;
  122.            
  123.             arrange();
  124.         }
  125.        
  126.         protected function updatePosFromValue():void
  127.         {
  128.             if (value>= min && value <= max)
  129.                 _fg.x = (value - min) * space / span + minPos;
  130.             else
  131.                 throw new Error("value (" + _value + ") is outside of range[" + min + "," + max + "]");
  132.         }
  133.        
  134.         protected function setValue(newValue:Number):void
  135.         {
  136.             var multiplier:Number = Math.pow(10, _props.decimals);
  137.             newValue *= multiplier;
  138.             newValue = Math.round(newValue);
  139.             newValue /= multiplier;
  140.            
  141.             _value = newValue;
  142.         }
  143.  
  144.         protected function doBtnDown(evt:MouseEvent):void
  145.         {
  146.             _timer.start();
  147.         }
  148.        
  149.         override protected function arrange():void
  150.         {
  151.             super.arrange();
  152.            
  153.             _bg.y = (_height - _bg.height) / 2;
  154.             _bg.x = 0;
  155.            
  156.             _fg.x = _pos - _fg.width / 2;
  157.         }
  158.  
  159.         override protected function doMouseUp(evt:MouseEvent):Boolean
  160.         {
  161.             if (_mouseDown)
  162.             {
  163.                 _timer.stop();
  164.                 dispatchEvent(new ValueEvent(ValueEvent.CHANGED, _value));
  165.             }
  166.  
  167.             return super.doMouseUp(evt);
  168.         }
  169.     }
  170. }

One Response to “Tutorial - Creating a reusable Slider Control / Component”

  1. chuchu says:

    Hi Martin,

    I like your application Fxtube want to implemented in my application but i try everything dont work still get this security error and i allready upload the proxy file please hellllllllllllllllllllllllllllllppppppppppp :)

    Thanx in advanced
    chuchu

Leave a Reply