12.14Tutorial - 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 backgroundbutton.png-- which contains the foreground
Then I create a small class, which I called GraphicAssets like so:
-
package assets
-
{
-
import flash.display.Bitmap;
-
/**
-
* ...
-
* @author Martin Legris
-
*/
-
-
public class GraphicAssets
-
{
-
[Embed(source="../../assets/bar.png")]
-
public static const BAR:Class;
-
-
[Embed(source="../../assets/button.png")]
-
public static const BUTTON:Class;
-
-
public static function getBitmap(template:Class):Bitmap
-
{
-
var inst:Object = new template();
-
var bitmap:Bitmap = new Bitmap(inst.bitmapData);
-
return bitmap;
-
}
-
}
-
}
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:
-
var button:Bitmap = GraphicAssets.getBitmap(GraphicAssets.BUTTON);
-
// or
-
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:
-
package newcommerce.controls
-
{
-
import flash.display.DisplayObject;
-
import flash.display.Sprite;
-
import flash.events.EventDispatcher;
-
-
/**
-
* ...
-
* @author Martin Legris ( http://blog.martinlegris.com )
-
*/
-
-
public class SpriteSliderProperties
-
{
-
protected var _bg:DisplayObject;
-
protected var _fg:DisplayObject;
-
-
protected var _min:Number;
-
protected var _max:Number;
-
protected var _stepSize:Number;
-
protected var _decimals:Number;
-
protected var _defaultValue:Number;
-
-
public function get fg():DisplayObject { return _fg; }
-
public function set fg(value:DisplayObject):void { _fg = value; }
-
-
public function get bg():DisplayObject { return _bg; }
-
public function set bg(value:DisplayObject):void { _bg = value; }
-
-
public function get min():Number { return _min; }
-
public function set min(value:Number):void { _min = value; }
-
-
public function get max():Number { return _max; }
-
public function set max(value:Number):void { _max = value; }
-
-
public function get stepSize():Number { return _stepSize; }
-
public function set stepSize(value:Number):void { _stepSize = value; }
-
-
public function get decimals():Number { return _decimals; }
-
public function set decimals(value:Number):void { _decimals = value; }
-
-
public function get defaultValue():Number { return _defaultValue; }
-
public function set defaultValue(value:Number):void { _defaultValue = value; }
-
-
public function SpriteSliderProperties(bg:DisplayObject, fg:DisplayObject, min:Number = 0, max:Number = 100, stepSize:Number = 10, decimals:Number = 0, defaultValue:Number = NaN)
-
{
-
_bg = bg;
-
_fg = fg;
-
-
_min = min;
-
_max = max;
-
_stepSize = stepSize;
-
-
_decimals = decimals;
-
-
if (isNaN(defaultValue))
-
_defaultValue = min;
-
else
-
_defaultValue = defaultValue;
-
}
-
}
-
}
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 buttonValueEvent.CHANGE-- called once the user is done moving the button
Here is sample code, our target:
-
var bg:Bitmap = GraphicAssets.getBitmap(GraphicAssets.BAR);
-
var fg:Bitmap = GraphicAssets.getBitmap(GraphicAssets.BUTTON);
-
-
var props:SpriteSliderProperties = new SpriteSliderProperties(bg, fg, 0, 100, 10, 1, 0);
-
var slider:SpriteSlider = new SpriteSlider(props);
-
addChild(_slider);
-
-
slider.addEventListener(ValueEvent.PREVIEW, doSliderValuePreview);
-
slider.addEventListener(ValueEvent.CHANGED, doSliderValueChanged);
-
-
slider.x = 0;
-
slider.y = (stage.stageHeight - _slider.height) / 2;
ValueEvent class
This class carries the value inside of it. Here is the source code:
-
package newcommerce.events
-
{
-
import flash.events.Event;
-
-
/**
-
* ...
-
* @author Martin Legris
-
*/
-
public class ValueEvent extends Event
-
{
-
public static const CHANGED:String = "value_changed_event";
-
public static const PREVIEW:String = "value_preview_event";
-
-
protected var _value:Number = 0;
-
public function get value():Number { return _value; }
-
-
public function ValueEvent(type:String, value:Number)
-
{
-
_value = value;
-
super(type);
-
}
-
}
-
}
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 componentinitEvents()-- adding event listeners- a
rrange()-- 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
-
package newcommerce.controls{
-
-
import flash.display.DisplayObject;
-
import flash.display.Sprite;
-
import flash.events.MouseEvent;
-
import flash.events.Event;
-
import ascb.drawing.Pen;
-
import newcommerce.drawing.AbstractDrawing;
-
-
public class BaseControl extends Sprite{
-
-
public var _width:Number = 0;
-
public var _height:Number = 0;
-
private var _padding:Number = 5;
-
protected var _mouseDown:Boolean = false;
-
protected var _drawing:AbstractDrawing;
-
-
public override function get width():Number
-
{
-
return _width;
-
}
-
-
public override function get height():Number
-
{
-
return _height;
-
}
-
-
public function get drawing():AbstractDrawing
-
{
-
return _drawing;
-
}
-
-
public function set drawing(drawing:AbstractDrawing):void
-
{
-
_drawing = drawing;
-
}
-
-
public function get padding():Number
-
{
-
return _padding;
-
}
-
-
public function set padding(padding:Number):void
-
{
-
_padding = padding;
-
}
-
-
// The setSize() method is automatically called when the authoring time instance is resized.
-
public function setSize(nW:Number, nH:Number):void {
-
scaleX = 1;
-
scaleY = 1;
-
_width = nW;
-
_height = nH;
-
arrange();
-
redraw();
-
}
-
-
public function getSize():Object
-
{
-
return {width:_width, height:_height};
-
}
-
-
-
public function BaseControl(initObj:Object)
-
{
-
for(var i:String in initObj)
-
{
-
this[i] = initObj[i];
-
}
-
-
init();
-
createChilds();
-
initEvents();
-
arrange();
-
redraw();
-
}
-
-
protected function init():void
-
{
-
if(_width <= 0)
-
_width = width;
-
if(_height <= 0)
-
_height = height;
-
-
scaleX = 1;
-
scaleY = 1;
-
}
-
-
protected function initEvents():void
-
{
-
addEventListener(MouseEvent.MOUSE_DOWN, doMouseDown);
-
addEventListener(MouseEvent.MOUSE_UP, doMouseUp);
-
addEventListener(Event.ADDED_TO_STAGE, doAddedToStage);
-
}
-
-
protected function createChilds():void
-
{
-
}
-
-
protected function doAddedToStage(evt:Event):void
-
{
-
stage.addEventListener(MouseEvent.MOUSE_UP, doParentRelease);
-
}
-
-
public function triggerRedraw():void
-
{
-
redraw();
-
}
-
-
public function rearrange():void
-
{
-
arrange();
-
}
-
-
protected function redraw():void
-
{
-
var pen:Pen = new Pen(graphics);
-
pen.clear();
-
pen.lineStyle(0,0,0);
-
pen.moveTo(0,0);
-
pen.beginFill(0,0);
-
pen.drawRect(0,0,_width,_height);
-
pen.endFill();
-
}
-
-
protected function arrange():void
-
{
-
x = Math.round(x);
-
y = Math.round(y);
-
}
-
-
protected function doMouseDown(evt:MouseEvent):void
-
{
-
_mouseDown = true;
-
}
-
-
protected function doMouseUp(evt:MouseEvent):Boolean
-
{
-
if(_mouseDown)
-
{
-
_mouseDown = false;
-
return true;
-
}
-
else
-
{
-
return false;
-
}
-
}
-
-
protected function initBtn(btn:*):void
-
{
-
btn.useHandCursor = true;
-
btn.buttonMode = true;
-
btn.mouseChildren = false;
-
}
-
-
public function doParentRelease(evt:MouseEvent):void
-
{
-
for(var i:Number = 0; i <this.numChildren; i++)
-
{
-
var child:DisplayObject = getChildAt(i);
-
if(child is BaseControl)
-
(child as BaseControl).doParentRelease(evt);
-
}
-
-
doMouseUp(evt);
-
}
-
-
public function mouseInside():Boolean
-
{
-
return (mouseX>= 0 && mouseX <= _width && mouseY>= 0 && mouseY <= _height);
-
}
-
}
-
}
Basically it has some functionality like:
- making sure the control is on a whole pixel (not
x:4.51andy:9.56for 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.
-
override protected function init():void
-
{
-
super.init();
-
_timer = new Timer(33, 0);
-
_timer.addEventListener(TimerEvent.TIMER, doTimer);
-
-
_value = _props.defaultValue;
-
}
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.
-
protected function spritify(obj:DisplayObject):Sprite
-
{
-
if (!(obj is Sprite))
-
{
-
var sprt:Sprite = new Sprite();
-
sprt.addChild(obj);
-
obj = sprt;
-
}
-
-
return obj as Sprite;
-
}
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:
-
override protected function createChilds():void
-
{
-
super.createChilds();
-
-
_bg = spritify(_props.bg);
-
_bg.name = "bg";
-
addChild(_bg);
-
initBtn(_bg);
-
-
_fg = spritify(_props.fg);
-
_fg.name = "fg";
-
addChild(_fg);
-
initBtn(_fg);
-
-
_width = _bg.width;
-
_height = Math.max(_fg.height, _bg.height);
-
}
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...
-
package newcommerce.controls
-
{
-
import flash.display.DisplayObject;
-
import newcommerce.events.ValueEvent;
-
import flash.display.Sprite;
-
import flash.events.MouseEvent;
-
import flash.events.TimerEvent;
-
import flash.utils.Timer;
-
-
/**
-
* @eventType newcommerce.events.ValueEvent.CHANGED
-
*/
-
[Event(name="value_changed_event",type="newcommerce.events.ValueEvent")]
-
-
/**
-
* @eventType newcommerce.events.ValueEvent.PREVIEW
-
*
-
*/
-
[Event(name="value_preview_event",type="newcommerce.events.ValueEvent")]
-
-
/**
-
* ...
-
* @author Martin Legris -- http://blog.martinlegris.com
-
*/
-
public class SpriteSlider extends BaseControl
-
{
-
protected var _bg:Sprite;
-
protected var _fg:Sprite;
-
-
protected var _value:Number;
-
protected var _pos:Number;
-
protected var _lastValue:Number;
-
-
protected var _timer:Timer;
-
-
protected var _props:SpriteSliderProperties;
-
-
public function get value():Number { return _value; }
-
public function set value(value:Number):void { _value = value; updatePosFromValue() }
-
-
override public function get width():Number { return super.width; }
-
override public function set width(value:Number):void { throw new Error("Cannot set the width manually on a SpriteSlider"); }
-
-
override public function get height():Number { return super.height; }
-
override public function set height(value:Number):void { throw new Error("Connot set the heigth manually on a SpriteSlider"); }
-
-
protected function get minPos():Number { return 0 + _fg.width / 2; }
-
protected function get maxPos():Number { return _width - _fg.width / 2; }
-
protected function get span():Number { return max - min; }
-
protected function get space():Number { return _bg.width - _fg.width; }
-
-
protected function get min():Number { return _props.min; }
-
protected function get max():Number { return _props.max; }
-
protected function get stepSize():Number { return _props.stepSize; }
-
-
public function get props():SpriteSliderProperties { return _props; }
-
-
public function SpriteSlider(props:SpriteSliderProperties)
-
{
-
_props = props;
-
super({});
-
}
-
-
override protected function init():void
-
{
-
super.init();
-
_timer = new Timer(33, 0);
-
_timer.addEventListener(TimerEvent.TIMER, doTimer);
-
-
_value = _props.defaultValue;
-
}
-
-
override protected function createChilds():void
-
{
-
super.createChilds();
-
-
_bg = spritify(_props.bg);
-
_bg.name = "bg";
-
addChild(_bg);
-
initBtn(_bg);
-
-
_fg = spritify(_props.fg);
-
_fg.name = "fg";
-
addChild(_fg);
-
initBtn(_fg);
-
-
_width = _bg.width;
-
_height = Math.max(_fg.height, _bg.height);
-
}
-
-
protected function spritify(obj:DisplayObject):Sprite
-
{
-
if (!(obj is Sprite))
-
{
-
var sprt:Sprite = new Sprite();
-
sprt.addChild(obj);
-
obj = sprt;
-
}
-
-
return obj as Sprite;
-
}
-
-
override protected function initEvents():void
-
{
-
super.initEvents();
-
_fg.addEventListener(MouseEvent.MOUSE_DOWN, doBtnDown);
-
_bg.addEventListener(MouseEvent.MOUSE_DOWN, doBtnDown);
-
}
-
-
protected function doTimer(evt:TimerEvent):void
-
{
-
_pos = mouseX;
-
_pos = Math.max(_pos, minPos);
-
_pos = Math.min(_pos, maxPos);
-
-
setValue((_pos - minPos) / space * span + min);
-
-
if(_value != _lastValue)
-
dispatchEvent(new ValueEvent(ValueEvent.PREVIEW, _value));
-
-
_lastValue = _value;
-
-
arrange();
-
}
-
-
protected function updatePosFromValue():void
-
{
-
if (value>= min && value <= max)
-
_fg.x = (value - min) * space / span + minPos;
-
else
-
throw new Error("value (" + _value + ") is outside of range[" + min + "," + max + "]");
-
}
-
-
protected function setValue(newValue:Number):void
-
{
-
var multiplier:Number = Math.pow(10, _props.decimals);
-
newValue *= multiplier;
-
newValue = Math.round(newValue);
-
newValue /= multiplier;
-
-
_value = newValue;
-
}
-
-
protected function doBtnDown(evt:MouseEvent):void
-
{
-
_timer.start();
-
}
-
-
override protected function arrange():void
-
{
-
super.arrange();
-
-
_bg.y = (_height - _bg.height) / 2;
-
_bg.x = 0;
-
-
_fg.x = _pos - _fg.width / 2;
-
}
-
-
override protected function doMouseUp(evt:MouseEvent):Boolean
-
{
-
if (_mouseDown)
-
{
-
_timer.stop();
-
dispatchEvent(new ValueEvent(ValueEvent.CHANGED, _value));
-
}
-
-
return super.doMouseUp(evt);
-
}
-
}
-
}

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
March 9th, 2010 at 1:35 pm