ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
XSlider - A Custom Component
http://www.actionscript.org/resources/articles/1108/1/XSlider---A-Custom-Component/Page1.html
Chris Bristol
I have been using Flash in my web development since version 4. As ActionScript became a more powerful language, I found myself fascinated by the interaction between it and PHP and MySQL. In recent years, I have worked at eliminating the use of the timeline altogether, building everything in pure AS3, and making my code reusable. The fantastic resources of the vast online Flash community has been a great source of help and inspiration. I hope to be able to give back a little of that. You can reach me via my website: Xty Digital Design
 
By Chris Bristol
Published on February 13, 2012
 
This all about freedom. The freedom to make components that do what You want for a change. The freedom to add a new feature whenever you need one. The freedom to use any development environment you want without compromise or sacrifice. The freedom to fly like an eagle. Well, maybe not, but that's up to you, isn't it? What we are going to do is make a slider component from scratch using 100% code.

Fly Like An Eagle
Recently I started using Flash Develop 4, and it has been a change I'm glad I made. Not that Flash Develop is perfect, but it is so much better that it is worth the few quirks it has. There are several ways to use Flash Develop if you are coming from a Flash IDE environment. This article, Beginner's Guide to Flash Develop really helped me get started. The majority of the work I do is the building of custom applications, and so I always have a need for some combination of the Flash components - buttons, datagrids, comboboxes, etc. In order to continue using these components I created a swc file to hold the library and imported it into Flash Develop. This is an easy way to do it, and 99% of the time this works flawlessly, but I did start to notice that some things were not responding correctly. I would find myself missing a button skin for instance, and it was this small lack of consistency that led me to creating my own set of custom components. The XSlider is one of these.

When making my own components my goals were: 1) to create things that did not rely on imported graphics; 2) to add in the features that I wished the Flash component versions had, and 3) Make skinning the components easy once and for all.  This tutorial was developed in Flash Develop 4, but as they are pure AS3 classes you can use them in any environment. All the files that make up this demonstration are in the zip file - sliderDemo.zip, which contains a Flash Develop project, and in the src folder is also an fla file for use in Flash CS5. One of the classes, SliderBubble.as, uses the Caurina Tweener package, which I have included. If you use a different tweening engine you can just import the correct package and change the tweening code to match. To use these files do one of the following: If you have your own class library set up, drop the included folders into it; or place the ca and caurina folders and the Main.as file in the same place as your fla file. Download the zip file, and let's get going.

The Demo

Below is a demonstration of several versions of the XSlider in action. Number one, titled Default Settings, is straight out of the box with no options applied, other than the slider bubble. Number 2, titled Custom Scale, uses an array of objects to create a custom scale. Number 3, titled No Scale, custom Min / Max values, constant update, does not show us a scale, uses custom min and max values, and also uses the constant update feature. Number 4, titled Custom colors, track length, divisions, min / max values, uses custom colors for the slider track and the triangle icon, a custom number of divisions, and a custom set of min and max values. We'll look at these in a bit more detail when we get into the document class, Main.as.


The Main.as Class

Open up the Main.as class from the zip file you downloaded. This class is the Document class that runs the whole show.
[as]
package {
    import flash.display.Sprite;
    import flash.events.*;
    import flash.text.*;
    import ca.xty.components.XSlider;
    import ca.xty.components.ShapeFactoryComp;
[/as]

First we import all the classes we need. We need the Sprite class from the flash.display package. Then we grab all the events classes. We do this by using an asterisk, which is saying, "Get me all the classes in the flash.events package". We do the same for the flash.text classes. Then we import two classes from the ca.xty.components package, XSlider - which builds and runs our slider component, and the ShapeFactoryComp class which handles the drawing of the various graphics we use here.

[as]
public class Main extends Sprite {
       
    private var redBox:Sprite;
    private var redBoxMin:Sprite;
    private var redBoxMax:Sprite;
    private var sf:ShapeFactoryComp = new ShapeFactoryComp();
       
    private var xs1:XSlider;
    private var xs2:XSlider;
    private var xs3:XSlider;
    private var xs4:XSlider;
       
    private var sArray:Array = [{Txt:"0", data:0}, {Txt:"100", data:100}, {Txt:"1K", data:1000}, {Txt:"10K", data:10000}, {Txt:"100K", data:100000}, {Txt:"1M", data:1000000}];
       
    private var t1:TextField;
    private var t2:TextField;
    private var t3:TextField;
    private var t4:TextField;
    private var t5:TextField;
    private var t6:TextField;
    private var t7:TextField;
   
    private var it1:TextField;
    private var it2:TextField;
    private var it3:TextField;
       
    private var tf:TextFormat;
    private var tfc:TextFormat;
       
    private var xPos:int;
    private var yPos:int;
[/as]

Next up we declare the variables we will be using. These are all set to private as they will only be used inside the Main class. Make note of the sArray. This is an array of Objects that will create a custom scale for our slider. Each Object, denoted by the curly braces, gives us two properties. The first property, Txt, is the string that will sit above the lines, and the second property, data, is the integer equivalent of the Txt property. In this example, {Txt:"1K", data:1000}, we use the string "1K" to represent the integer value of 1000. We do this because we have limited space for the text.

[as]
public function Main():void {
           
    tf = new TextFormat();
    tf.size = 12;
    tf.color = 0x000000;
    tf.font = "Verdana";
    tf.align = "left";
           
    tfc = new TextFormat();
    tfc.size = 12;
    tfc.color = 0x000000;
    tfc.font = "Verdana";
    tfc.align = "center";
           
    xPos = 30;
    yPos = 20;
           
    t1 = new TextField();
    t1.x = xPos;
    t1.y = yPos;
    t1.width = 200;
    t1.height = 20;
    t1.text = "1) Default Settings";
    t1.setTextFormat(tf);
    addChild(t1);
           
    xPos += 20;
    yPos += 60;
[/as]

After the constructor, we set the properties for our two TextFormats, tf and tfc. The only difference between these two is the align property, which in the case of the tfc format is declared as "center". Then we establish our starting x and y values by declaring the xPos and yPos variables. These will look familiar if you have read any of my other tutorials. Basically they are an easy way to change your mind. By using these variables I can lower the entire set up by 10 pixels if I want by simply changing the initial yPos value from 20 to 30. Doing it this way I don't have to visit every TextField, etc and change their y properties individually.

Now let's look at the first slider.

[as]
xs1 = new XSlider();
xs1.x = xPos;
xs1.y = yPos;
xs1.useSliderBubble = true;
xs1.addEventListener(XSlider.SLIDER_READY, getSlider1Num);
addChild(xs1);
[/as]

This the basic slider using all the default settings except the useSliderBubble. By setting this property to true, we will make use of this feature. Notice that the event listener uses a custom event SLIDER_READY.

Example 2

[as]
xs2 = new XSlider();
xs2.x = xPos;
xs2.y = yPos;
xs2.useSliderBubble = true;
xs2.scaleMarker = sArray;
xs2.addEventListener(XSlider.SLIDER_READY, getSlider2Num);
addChild(xs2);
[/as]

In this example we are defining the property scaleMarker and populating it with our custom array, sArray. This will force the slider to use this for the scaling graphics and for all the calculations.

Example 3

[as]
redBoxMin = sf.createSprite(0xff0000, 10, 10);
redBoxMin.x = xPos;
redBoxMin.y = yPos;
addChild(redBoxMin);
           
xPos += 25;
yPos += 10;
           
xs3 = new XSlider();
xs3.x = xPos;
xs3.y = yPos;
xs3.useSliderBubble = true;
xs3.useScale = false;
xs3.minNum = 1;
xs3.maxNum = 2;
xs3.constantUpdate = true;
xs3.addEventListener(XSlider.SLIDER_READY, scaleBox);
addChild(xs3);
           
xPos += 105;
yPos -= 20;
           
redBoxMax = sf.createSprite(0xff0000, 20, 20);
redBoxMax.x = xPos;
redBoxMax.y = yPos;
addChild(redBoxMax);

xPos += 150;
           
redBox = sf.createScalableRect(0xff0000, 50, 50);
redBox.x = xPos;
redBox.y = yPos;
addChild(redBox);
[/as]

This example makes different use of the slider. Instead of determining a picked number we are using the slider to scale the redBox Sprite. The other two red boxes, one on either side of the slider, graphically represent the scale. To create the slider xs3 we set the useScale property to false, set the minNum to 1 and the maxNum to 2, and set the constantUpdate property to true. Our min and max values mean that we can double the size of the large red box. A scale of 1 is the normal state for the red box, and the max value of 2 allows us to increase the scale by double. The property constantUpdate dispatches the event every time the the triangle icon is moved. We don't need a scale in this case because we are setting the desired size of the box visually. We have the useSliderBubble property set to true, but normally I would set it to false. I turned it on here to demonstrate the the toFixed() function. You can see that the number reported in the bubble never has more than two digits after the decimal point.

Example 4

[as]
xs4 = new XSlider();
xs4.x = xPos;
xs4.y = yPos;
xs4.useSliderBubble = true;
xs4.minNum = 0;
xs4.maxNum = 500;
xs4.trackWidth = 200;
xs4.numDivisions = 10;
xs4.trackColor = [0xfabab8, 0xff0000];
xs4.iconColor = [0xb8fad3, 0x02d120];
xs4.addEventListener(XSlider.SLIDER_READY, getSlider4Num);
addChild(xs4);
[/as]

Our last example shows off a few of the other properties available for customizing the XSlider component. We have set the min and max values to give us a range between 0 - 500. The trackWidth property is set to 200, the numDivisions is set to 10, and the trackColor and iconColor have been assigned a red gradient and a green gradient.

Functions
[as]
private function getSlider1Num(e:Event):void {
    var sNum:Number = xs1.passNum;
    it1.text = String(sNum);
}
       
private function getSlider2Num(e:Event):void {
    var sNum2:Number = xs2.passNum;
    it2.text = String(sNum2);
}
       
private function scaleBox(e:Event):void {
    var sNum3:Number = xs3.passNum;
    redBox.scaleX = sNum3;
    redBox.scaleY = sNum3;
}
       
private function getSlider4Num(e:Event):void {
    var sNum4:Number = xs4.passNum;
    it3.text = String(sNum4);
}
[/as]

These four functions handle the SLIDER_READY events fired by the XSlider class. Numbers 1, 2, and 4 work the same, by taking the public variable passNum from the XSlider class, turning it into a String and then displaying it in the appropriate TextField. Function number 3 works slightly different. The slider xs3 has the constantUpdate property set to true and so it is firing the event constantly. Here we take the passNum value and apply it to the redBox's scaleX and scaleY properties to expand and contract the redBox's size.

Now that we've seen our slider at work, let's take a look at the XSlider class itself.

The XSlider.as Class
This class lives in a folder called components. The first thing to notice is the package declaration - ca.xty.components. This tells us that in a folder called ca, there is another folder called xty, and in that folder is a folder called components. This is a piece of my class library, and if you don't already have a library of your own, you should seriously consider building one. We use other classes in the XSlider class such as ShapeFactoryComp and SliderBubble, but we don't have to import these classes to use them since they all live in the same folder, components. We do need to import the classes within the folders display, events and text. Using the asterisk allows us to import all the classes in these folders.

[as]
package ca.xty.components {
    import flash.display.*;
    import flash.events.*;
    import flash.text.*;
[/as]

Next we declare the public and private variables we will use within the XSlider class.

[as]
public class XSlider extends MovieClip {
        //This is the variable we use in the Event we use to tell our program the slider is ready
        public static const SLIDER_READY:String = "SLIDER_READY";
       
        //The following public variables are used to customize the look and actions of the slider. They all have default values so that you are not required to explicitly set any of them.
        public var passNum:Number = 0;// This is the variable used to pass on the result obtained from the slider
        public var trackColor:Array = [0xf0f0f0, 0x7b7b7b];// This sets the track color. The drawing application requires this to be a gradient so we need a two number array.
        public var iconColor:Array = [0xf0f0f0, 0x7b7b7b];// This sets the gradient color of the triangle icon
        public var strokeColor:Number = 0x989898;// This is the stroke - line - color. It is used in building the scale, the track and the triangle icon
               
        public var iconDiameter:int = 10;// This is the diameter of the triangle icon
        public var trackWidth:int = 100;// This is the track width
        public var trackHeight:int = 2;// This is the track height - remember there is also a 1 pixel line drawn
        public var strokeWidth:int = 1;// This is the stroke width and is used in the drawing of the scale, track and triangle icon
       
        public var minNum:Number = 0;// The minimum number used in establishing the overall range of the slider
        public var maxNum:Number = 100;// The maximum number
        public var numDivisions:int = 5;// This sets up how many divisions the scale is divided into
       
        public var useScale:Boolean = true;// This sets whether or not the scale is used
       
        public var scaleMarker:Array = new Array();// This array creates a custom scale
       
        public var useSliderBubble:Boolean = false;// This sets whether we use the SliderBubble or not
       
        public var constantUpdate:Boolean = false;// This determines how often we dispatch the SLIDER_READY event. A setting of false dispatches the event only when the icon has stopped moving
       
        //The private variables below are the internal variables for use within this class only.
        private var useLocalScale:Boolean = false;
        //SliderBubble is a class which produces the bubble we use to indicate the number the user is over.
        private var sb:SliderBubble;
        private var sbOn:Boolean = false;
        private var numAt:Number = 0;
        private var posMultiplier:Number;
        private var divNum:int = 0;
       
        private var xFactor:int;
        private var markerX:int;
        private var markerY:int;
        private var valueFactor:Number;
       
        private var scaleTxt:Array = new Array();
        private var scaleMarkerX:int;
        private var scaleMarkerY:int;
        private var scaleFormat:TextFormat;
       
        private var triangleIcon:Sprite;
        private var track:Sprite;
        private var scale:Sprite;
        //The ShapeFactoryComp class provides the graphics for our slider.
        private var sf:ShapeFactoryComp = new ShapeFactoryComp();
       
        //These are the variables we use in our loops.
        private var i:uint;
        private var len:uint;
[/as]

First of all we declare our public variables. The public designation means that these variables are accessible outside of the XSlider class. This enables us to set various properties such as trackColor and iconColor and therefore we can customize the slider we create. The variable passNum is the variable we will use to pass the result to the Main.as class. Because it is public, passNum can be picked up as we have seen in the Main class by referencing the instance of the XSlider class like so: xs1.passNum. The public static const SLIDER_READY variable is a reference we use when we dispatch the event that tells our Main class that the variable passNum is ready to be picked up. The comments explain the use of each of the public variables.

The private variables are the bits and pieces we will use only inside the XSlider class.

The Constructor:

[as]
public function XSlider() {
    //Once the slider instance has been added to the stage using addChild we proceed with building the slider via the init function.
    addEventListener(Event.ADDED_TO_STAGE, init);
}
[/as]

Until I started building these components I never fully appreciated the event ADDED_TO_STAGE. What this means for us is that we can instantiate the XSlider, and because it now exists we can set it's public properties before we set about physically creating it. The other way to do this is to pass the properties to the XSlider class via the constructor, but just imagine having to type in all of them every time you create a new slider. This way, because they all have default values, we don't have to type anything if we want to use it straight out of the box. So, we add the event listener which will run the init function once our instance of the XSlider has been physically added to the stage using the addChild() method.

[as]
private function init(e:Event = null):void{
    //First thing we do is remove the event listener that got us here.
    removeEventListener(Event.ADDED_TO_STAGE, init);
           
    //Here we set up out TextFormat
    scaleFormat = new TextFormat();
    scaleFormat.size = 8;
    scaleFormat.color = 0x000000;
    scaleFormat.font = "Verdana";
    scaleFormat.align = "center";
           
    //If our scaleMarker array has length, we set our variable numDivisions to equal the scaleMarker length minus 1.
    if(scaleMarker.length){
        numDivisions = scaleMarker.length - 1;
    }
    //Our xFactor variable is calculated to equal our track width divided by our numDivisions
    xFactor = trackWidth / numDivisions;
[/as]

The first job in the init function is to remove the ADDED_TO_STAGE event listener. Then we set the properties of our TextFormat, scaleFormat. Now we check to see if the scaleArray has length. When we declared this variable we created an empty array, so unless we have passed in a custom array, then the length will be 0. If it does have length, then we set our numDivisions variable to equal the scaleMarker array's length minus one. Why minus one? If we have 5 divisions then we have six lines. The scaleMarker array holds the information to build those lines, so we will want our numDivisions to always be one less than the length of our scaleMarker array. Next we give our xFactor variable the value of trackWidth divided by numDivisions. If we are using the default settings, this would equal 20: trackWidth (100) divided by numDivisions (5). This tells us the number of pixels between each marker, something we will make use of shortly.

Now we'll set up our scale - or not.

[as]
//If useScale is set to true - meaning we want to show a scale on stage - we first check to see if the scaleMarker array contains anything.
if(useScale){
    //If the scaleMarker array does not contain anything, we put together a standard array to use.
    if(!scaleMarker.length){
        //We set our variable useLocalScale to true.
        useLocalScale = true;
        divNum = maxNum / numDivisions;
        //Our length for the loop we are about to do is set to numDivisions plus 1 because we want one more line than the number of divisions.
        len = numDivisions + 1;
        //The variable t is a String representation of the scale number, and the variable sn is the integer version.
        var t:String = String(minNum);
        var sn:Number = minNum;
        for (i = 0; i < len; i++){
            var sObj:Object = {Txt:t, data:sn};
            scaleMarker.push(sObj);
            sn += divNum;
            //sn += xFactor;
            t = String(sn);
        }
    }
    // scale is the Sprite that will hold all the bits that make up our scale graphic.
               
    scale = new Sprite();
    scale.graphics.lineStyle(1, strokeColor, 1);
    markerX = 0;
    len = numDivisions + 1;
    for (i = 0; i < len; i++){
        scale.graphics.moveTo(markerX, -12);
        scale.graphics.lineTo(markerX, 0);
        markerX += xFactor;
    }
               
    //Next we put the TextFields in place. I am making them 20 pixels wide by 16 pixels high. I want them to be centered over the lines we just drew, so we'll start the scaleMarkerX
    // variable at -10. We want them to sit just on top of the lines. The top of the lines are at -12 and the TextFields are 16 pixels high so we set the scaleMarkerY at -28.
    scaleMarkerX = -10;
    scaleMarkerY = -28;
    len = scaleMarker.length;
    for (i = 0; i < len; i++){
        scaleTxt[i] = new TextField();
        scaleTxt[i].x = scaleMarkerX;
        scaleTxt[i].y = scaleMarkerY;
        scaleTxt[i].width = 20;
        scaleTxt[i].height = 16;
        scaleTxt[i].autoSize = TextFieldAutoSize.CENTER;
        scaleTxt[i].text = scaleMarker[i].Txt;
        scaleTxt[i].setTextFormat(scaleFormat);
        scale.addChild(scaleTxt[i]);
                   
        scaleMarkerX += xFactor;
    }
               
    addChild(scale);
}else{
    //If we are not using our scale graphic we want to set our posMultiplier variable. This variable will help us determine the number the user is currently over.
    //If minNum is equal to 0 we simply divide our maxNum by the track width. If minNum is not equal to 0, we subtract the minNum from the maxNum and then divide it by the track width.
    if(minNum == 0){
        posMultiplier = maxNum / trackWidth;
    }else{
        posMultiplier = (maxNum - minNum) / trackWidth;
    }
}
[/as]
The first thing to determine is whether useScale is true or false. If it is true, then we next need to check and see if we are using a custom scale buy seeing if the scaleMarker array has length. If it does not have length, then we will be using a locally generated scale, so we set our useLocalScale variable to true. Now we need to determine our variable divNum in case we are using a custom min/max number. To calculate this we divide our maxNum by the numDivisions. Then we set the len variable to numDivisions plus one in order to determine how many lines we will need to draw. Remember, the number of lines will always be one more than the number of divisions. Now we set up to local vars - t as a String and sn as a Number, and initially set them to our minNum variable. The t variable will be what we use to populate the TextField and the sn variable will be what use to make our calculations. With all of these things figured out we run our for loop. We create an Object with two properties - Txt for the label, and data for the number and assign them our t and sn variables. Then we push this Object into our scaleMarker array. Once that is done, we add the value of divNum to the sn variable and then set t to equal the String equivalent of sn.

Then we move on and create the lines for our scale. First we create a new Sprite called scale, and then assign a lineStyle to it's graphics property. The lineStyle has three properties: first is the line thickness, then the line color and finally the line alpha. We want these lines to be 1 pixel wide, set to our strokeColor variable, and with an alpha of 1. To get started we set our markerX variable to 0 and our len variable to numDivisions plus one. In the for loop we start by using the moveTo function to determine the starting x and y properties. The x property will be set by using our markerX variable. Each line will be 12 pixels high so our y property is set to -12. In the next line we draw the line using the lineTo function. This line is straight up and down, so the x property remains constant, but the y property is set to 0 which will intersect with our track. To prepare for the next line to be drawn we add our xFactor (the number of pixels between divisions) to our markerX and loop back to the beginning.

With our lines in place, we now construct the TextFields. Our default setting for the trackWidth is 100, and our default setting for the numDivisions is 5, which gives us an xFactor of 20. With this in mind I am creating each TextField to be 20 pixels wide, with a height of 16 pixels. In order to center the TextFields over each line we set our initial scaleMarkerX variable to be -10. The scaleMarkerY variable is set to -28 because the lines are 12 pixels high and the TextFields are 16 pixels high: 12 + 16 = 28, and since they are going to be above our track, which has a y position of 0, we will need to make that 28 a minus 28. We use our array scaleTxt to hold each TextField, and the reason we use an array at all is because we never know how many TextFields we will be needing, and this method allows us to have as many as we need. The first line in our for loop creates a new TextField and assigns it to the i index of the scaleTxt array. Then we assign the x and y properties, and the width and height properties. Then, in case we require a TextField wider than 20 we set the autoSize property to scale the TextField using a value of CENTER. This will expand the TextField equally to the left and right thereby keeping it centered over of line. Next we assign the text property using the Txt property at i index of our scaleMarker array, and set the TextFormat to our scaleFormat variable. Now we add it as a child of our scale Sprite. With all that done, we update the scaleMarkerX property by adding our xFactor to it. When the for loop finishes running, we use addChild(scale) to place the whole thing on stage.

But, if useScale is false then we need to set our variable posMultiplier so that we can properly calculate the number we want to send back to our Main class. The posMultiplier is used to match our scale. What we are measuring is the x position of the triangle icon. Using our default settings - minNum:0, maxNum:100, and trackWidth:100, our posMultiplier would be 1 - maxNum divided by trackWidth. However, if our maxNum was 200 then we would have a posMultiplier of 2, or if our maxNum was 100, but our trackWidth was 200 then we would have a posMultiplier of .5. The first thing we need to determine is if our minNum is equal to zero. If it is then we can simply divide our maxNum by our trackWidth. If minNum is not equal to zero, then we need to first subtract our minNum from our maxNum and then divide the result by our trackWidth. For example, if our minNum was 100 and our maxNum was 500 then our scale range is 400. Dividing this by our trackWidth of 100, our posMultiplier would be 4. Scratching your head because if we simply divide the maxNum (500) by the trackWidth (100) then on the far right of the slider would be 500, right? Right, but what we have to consider here is the minNum (100). On the far left instead of having 0, we need to show 100. As you'll soon see, it will all work out.

Last, but not least we need to build our track and triangle icon.

[as]
track = sf.createHorzGradWithLine(trackColor, strokeColor, strokeWidth, trackWidth, trackHeight);
track.x = 0;
track.y = 0;
addChild(track);
//And here we build our triangle icon. We set up the MOUSE_DOWN and MOUSE_UP event listeners so we can start and stop the drag feature.
triangleIcon = sf.createSliderTriangle(iconColor, strokeColor, iconDiameter );
triangleIcon.x = 0;
triangleIcon.y = 0;
triangleIcon.buttonMode = true;
triangleIcon.useHandCursor = true;
triangleIcon.addEventListener(MouseEvent.MOUSE_DOWN, onStartMove);
triangleIcon.addEventListener(MouseEvent.MOUSE_UP, onStopMove);
addChild(triangleIcon);
[/as]

We start by building our track. Through our ShapeFactoryComp class (which we will look at in a minute) we use the function createHorzGradWithLine. The properties this function takes are trackColor, strokeColor, strokeWidth, trackWidth, and trackHeight. We set the x and y properties to 0 and use addChild to put it on stage.

Next we build our triangle icon using the ShapeFactoryComp class's createSliderTriangle function whose properties are iconColor, strokeColor, and iconDiameter. We also set the x and y properties to 0. In order to use a hand cursor icon we set the buttonMode property to true and then the useHandCursor property to true as well. The triangle icon runs the whole show, so we add two event listeners, MOUSE_DOWN and MOUSE_UP, which will control the drag functions.

Now that all the bits are in place, let's look at the functions which bring the whole thing to life.

[as]
private function onStartMove(event:MouseEvent):void{
    triangleIcon.startDrag();
    addEventListener(Event.ENTER_FRAME, monitorMove);
    stage.addEventListener(MouseEvent.MOUSE_UP, onStopMove);
}
[/as]

Our MOUSE_DOWN event triggers the onStartMove function, where we declare the startDrag() function and attach it to the triangleIcon. Now we add two more event listeners, ENTER_FRAME, and a MOUSE_UP event attached to the stage instance. The ENTER_FRAME event will constantly monitor our triangleIcon's movements. The second MOUSE_UP event is added to the stage so that even if your mouse is no longer over the triangle icon, we will still be able to stop the dragging process.

[as]
private function monitorMove(e:Event):void {
    if(triangleIcon.y != 0){
        triangleIcon.y = 0;
    }
   
    if(triangleIcon.x < 0){
        triangleIcon.x = 0;
    }
   
    if(triangleIcon.x > trackWidth){
        triangleIcon.x = trackWidth;
    }
   
    numAt = triangleIcon.x;
           
    if(useScale){
        if(useLocalScale){
            valueFactor = scaleMarker[1].data / xFactor
            passNum = numAt * valueFactor;
        }else{
            len = scaleMarker.length;
            for (i = 0; i < len; i++){
                if(numAt <= xFactor){
                    valueFactor = scaleMarker[i + 1].data / xFactor;
                    passNum = numAt * valueFactor;
                    break;
                }else if(numAt > xFactor && numAt <= xFactor * i){
                    valueFactor = (scaleMarker[i].data - scaleMarker[i-1].data) / xFactor;
                    passNum = ((numAt - (xFactor * (i-1))) * valueFactor) + scaleMarker[i-1].data;
                    break;
                }
            }
        }
    }else{
        if(minNum == 0){
            passNum = numAt * posMultiplier;
        }else{
            passNum = (numAt * posMultiplier) + minNum;
        }
    }
   
    if(useSliderBubble){
        if(!sbOn){
            sb = new SliderBubble(0xffffff, 0xff0000, 50, String(passNum.toFixed(2)));
            sb.x = mouseX - (sb.width / 2);
            sb.y = mouseY - 40;
            addChild(sb);
            sbOn = true;
        }else{
            sb.updateSliderTxt(String(passNum.toFixed(2)));
            sb.x = mouseX - (sb.width / 2);
        }
    }
   
    if(constantUpdate){
        dispatchEvent(new Event(XSlider.SLIDER_READY, true));
    }
}
[/as]

The first order of business is to make sure our triangle icon is behaving. We want to keep the y position of the icon on 0, so if it happens to wander off, we stick it right back. Next we look at the x position of the triangle icon. Here we check to see if the icon's x position is less than 0 and make it equal to 0 if it is. And, going the other way, to keep the icon from running off the end of the track we check to see if it's x position is greater than the track width and set it to equal the track width if it is.

Next, we set our variable numAt to record the x position of the triangle.

Our next job is to figure out the number we need to pass back to the main application, and set our passNum variable equal to that. We have a few different scenarios to consider here. If useScale is true, then we will set our variable called valueFactor. And, if we are using our scale graphic, then we need to check and see if useLocalScale is true. If useLocalScale is true, our calculation is fairly simple. We take the scaleMarker array's second data property [1] and divide it by xFactor to determine the valueFactor. Since this is our own locally made scaleMarker array we can be certain that the values between markers will be consistent. Once we have figured out the correct valueFactor, we determine our passNum by multiplying our numAt times valueFactor.

If we are not using local scale, we are therefore using a custom scaleMarker array, and we calculate the valueFactor in a slightly different way. Since we cannot be sure of the difference between markers we first need to determine where we are. First we check to see if numAt is less than or equal to our xFactor. If it is then we know we are between the first marker and the second marker so we can get the data property from the second object in the scaleMarker array by adding i plus 1, which in this case is really 0 plus 1, then we divide that value by our xFactor. Now we calculate our passNum by multiplying the numAt times the valueFactor. Now that we have a result, we then break out of the loop.

If our numAt is not less than or equal to xFactor, then we need to look further ahead. We check now to see if numAt is greater than xFactor and less than or equal to xFactor times i. Once we have found our correct range, our valueFactor is calculated by dividing the result of the current scaleMarker object's data property (i) - the previous scaleMarker object's data property (determined by i - 1) by our xFactor. Once we determine the valueFactor value we proceed to calculate our passNum. First we subtract our xFactor times i minus 1 from numAt, multiply that times our valueFactor and add the previous scaleMarker object's data property. Let's do a quick example using our custom scaleMarker array. Let's say that the triangleIcon is halfway (30 pixels) between the second and third markers, that is between 100 and 1000. Our valueFactor is calculated like this: (1000 - 100) / 20 which equals 45. Our passNum is then calculated like so: (30 - (20 * 1) * 45) + 100 which equals 550. Now that we have a result, we break out of the loop.

If we are not using scale, then we need to determine what our minNum is. If it is 0, the default, then our passNum is simply the numAt times the posMultiplier. If minNum does not equal 0 then we have a custom set of parameters, so we take the result of the numAt times the posMultiplier and add the minNum to it. For example: if your custom range is from 100 - 200 and you are in the very middle you want the number returned to be 150. So, numAt(50) times posMultiplier(1) plus the minNum (100) equals 150.

Now that we have our result we need to check if useSliderBubble is true. If it is, then we create the bubble and pass it the passNum variable we just figured out above cast as a String. We need to also see if the sliderBubble is already showing, and if it is, then we only want to update the number, not create another bubble. In order to avoid passing numbers like 1.9988762534, we apply the method toFixed(2) to the passNum. This will give us a maximum of 2 numbers after the decimal point. Note that passNum is not affected here, only the version which we are showing in the TextField.

The last thing to check in this function is whether constantUpdate is true or false. If the constantUpdate variable is true we fire off an event to the main application telling it to pick up the public variable passNum.

[as]
private function onStopMove(event:MouseEvent):void{
    stopDrag();
    removeEventListener(Event.ENTER_FRAME, monitorMove);
    stage.removeEventListener(MouseEvent.MOUSE_UP, onStopMove);
    if(useSliderBubble){
        removeChild(sb);
        sbOn = false;
    }
    if(!constantUpdate){
        dispatchEvent(new Event(XSlider.SLIDER_READY, true));
    }
}
[/as]

The final function in this class deals with stopping the drag on the triangleIcon. After declaring stopDrag() we remove the ENTER_FRAME and MOUSE_UP event listeners. If useSliderBubble is true, we also remove it from the stage and set the Boolean variable sbOn to false. And, finally, if constantUpdate is not true, then we dispatch our event to tell the Main class it's time to pick the passNum variable.

The XSlider class makes use of a couple of other classes within the components folder, ShapeFactoryComp and SliderBubble. We'll take a look at those next.

Out Of Thin Air - The ShapeFactoryComp Class
This class handles the drawing of the bits and pieces for all of the components I have made. It means that I don't need to import any graphics - we are making everything with pure code, out of thin air as it were. Skinning traditional components has taken a few different shapes over the years, but really, it was always a pain. Drawing your own graphics from scratch makes it easy to set up the colors, change the thickness, length, or even the shape of your component pieces. Suddenly skinning is no longer a separate task.

The ShapeFactoryComp class consists entirely of public functions. Once you create an instance of this class - private var sf:ShapeFactoryComp = new ShapeFactoryComp(); - then you access the functions like so: somePiece = sf.createSpriteWithLine(). Out of the many functions here, the XSlider class only uses a couple of them, which we will look at below.

[as]
public function createHorzGradWithLine(ca:Array, lineColor:Number, lineWidth:int, w:int, h:int, x:int = 0, y:int = 0):Sprite{
    var s:Sprite = new Sprite();
           
  var fillType:String = GradientType.LINEAR;
    var colors:Array = ca;
    var alphas:Array = [1, 1];
    var ratios:Array = [0, 255];
    var matrix:Matrix = new Matrix();
    var gradWidth:Number = w;
    var gradHeight:Number = h;
    var gradRotation:Number = 90 / 180 * Math.PI;
    var gradOffsetX:Number = 0;
    var gradOffsetY:Number = 0;
           
    matrix.createGradientBox(gradWidth, gradHeight, gradRotation, gradOffsetX, gradOffsetY);
    var spreadMethod:String = SpreadMethod.PAD;
    s.graphics.beginGradientFill(fillType, colors, alphas, ratios, matrix, spreadMethod);
    s.graphics.lineStyle(lineWidth, lineColor, 1);
    s.graphics.drawRect(0, 0, w, h);
    s.graphics.endFill();
    s.x = x;
  s.y = y;
  return s;       
}
[/as]

This is the function we use to draw our track. Of the seven possible properties you can pass in, only the first five are required. You need to supply an array of two colors, a single color for the lineColor, an integer for the lineWidth, and an integer for the width - w - and the height - h. As you can see this function returns a Sprite, so the first thing we do is create a new Sprite. Then we have a bunch of variables connected with creating a gradient. Then using the graphics property of our Sprite - s - we begin the gradient fill, set up our lineStyle, draw the rectangle and finish with the endFill. Then we set the x and y properties of our Sprite and finally we return it.

The next function we use creates our triangleIcon.

[as]
public function createSliderTriangle(ca:Array, lc:Number, d:int = 10):Sprite{
    var tn:Sprite = new Sprite();
    var fillType:String = GradientType.LINEAR;
    var colors:Array = ca;
    var alphas:Array = [1, 1];
    var ratios:Array = [0, 255];
    var matrix:Matrix = new Matrix();
    var gradWidth:Number = d;
    var gradHeight:Number = d;
    var gradRotation:Number = 90 / 180 * Math.PI;
    var gradOffsetX:Number = 0;
    var gradOffsetY:Number = 0;
           
    matrix.createGradientBox(gradWidth, gradHeight, gradRotation, gradOffsetX, gradOffsetY);
    var spreadMethod:String = SpreadMethod.PAD;
    tn.graphics.beginGradientFill(fillType, colors, alphas, ratios, matrix, spreadMethod);
    tn.graphics.lineStyle(1, lc, 1);
    tn.graphics.moveTo(0, -(d/2));
    tn.graphics.lineTo((d/2), d/2);
    tn.graphics.lineTo(-(d/2), d/2);
    tn.graphics.lineTo(0, -(d/2));
    tn.graphics.endFill();
    return tn;
}
[/as]

The function createSliderTriangle has only 3 possible properties you can manipulate, and only the first two are required. In the demo we have a variable called iconDiameter and we include it in the function to give us the flexibility to change the size of the triangle icon easily. Once again you provide an array of two colors, a single color for the line color - lc - and an optional diameter for the triangle. It works in much the same way as the last function we looked at. Create a new Sprite, set the variables for the gradient, start drawing with beginGradientFill, and set up our lineStyle. The actual drawing of the triangle shape comes up next. I want the triangle to have a centered registration point. To get ready to draw we move our line to a position where x = 0 and y = the property d divided by two and set to be negative. Using the default value of 10, this means that our first line will be drawn starting at x:0, y:-5. The first line is drawn to the co-ordinates x:d/2 or 5, y: d/2 or 5. This gives us the right hand side of the triangle. Our next co-ordinates are x:-(d/2) or - 5, y:d/2 or 5. This is the bottom of the triangle. Our last co-ordinates are x:0, y:-(d/2) or -5. This is the left hand side of the triangle and now we are back to the beginning, leaving us with the triangle's center point being 0, 0. The reason this is desirable is because I want the triangle's tip to match up with the scale lines. so, at the 0 point you notice that the left hand side of the triangle is hanging over the end, but the triangle's point is dead on 0. We end the fill and return the finished sprite.

The other class we will look at is the SliderBubble class which provides us with the bubble announcing the current position of the triangleIcon on the slider track.

[as]
package  ca.xty.components{
    import flash.display.*;
    import flash.events.*;
    import flash.text.*;
    import flash.geom.Matrix;
    import caurina.transitions.Tweener;
[/as]

No surprises here. Our package declaration puts this class in the components folder. The usual suspects are imported, with the addition of the Tweener tweening classes.

[as]
public class SliderBubble extends Sprite{
       
    private var _bubbleColor:Number;
    private var _lineColor:Number;
    private var _bubbleWidth:int;
    private var _bubbleText:String;
    private var _align:String;
    private var _txtSize:int;
    private var _lineSize:int;
       
    private var popUpPanel:Sprite;
    private var bg:Sprite;
   
    private var ppTF:TextField;
    private var bubbleFormat:TextFormat;
       
    private var sf:ShapeFactoryComp = new ShapeFactoryComp();
[/as]

We create the variables we will be needing next. They are all designated as private since we will only be using within this class. The first group of variables are all preceded by an underscore. This is simply my system for identifying which variables are passed in via the constructor function.

[as]
public function SliderBubble(BubbleColor:Number, LineColor:Number, BubbleWidth:int, BubbleText:String, Align:String = "center", TxtSize:int = 9, LineSize:int = 2) {
           
    _bubbleColor = BubbleColor;
    _lineColor = LineColor;
    _bubbleWidth = BubbleWidth;
    _bubbleText = BubbleText;
    _align = Align;
    _txtSize = TxtSize;
    _lineSize = LineSize;
           
    bubbleFormat = new TextFormat();
    bubbleFormat.font = "verdana";
    bubbleFormat.size = _txtSize;
    bubbleFormat.color = 0x000000;
    bubbleFormat.align = _align;
[/as]

We receive our properties via the constructor and assign them to the local private variables we created a moment ago. Then we set up our TextFormat, bubbleFormat.

[as]
    popUpPanel = new Sprite();
    addChild(popUpPanel);
    // Initially we set the popUpPanel's alpha to 0
    popUpPanel.alpha = 0;
    //This is our main background - the one whose colors you set up in the constructor
    bg = sf.createSpriteWithLine(_bubbleColor, _lineColor, _lineSize, _bubbleWidth, 20);
    bg.x = 0;
    bg.y = 0;
    popUpPanel.addChild(bg);
           
    ppTF = new TextField();
    ppTF.x = bg.x + 3;
    ppTF.y = bg.y + 2;
    ppTF.width = bg.width - 6;
    ppTF.height = bg.height - 4;
    ppTF.autoSize = TextFieldAutoSize.CENTER;
    ppTF.text = _bubbleText;
    ppTF.setTextFormat(bubbleFormat);
    popUpPanel.addChild(ppTF);
           
    if(ppTF.width > bg.width - 6){
        bg.width = ppTF.width + 6;
        ppTF.x = bg.x + 3;
    }
           
    Tweener.addTween(popUpPanel, {alpha:1, time:1, transition:"easeOutExpo"});
}
[/as]

The first piece to make is a Sprite to hold all the pieces. Using this as a container it makes it easy to dispose of everything when the time comes. For the moment we want this to be invisible, so we set the popUpPanel's alpha property to zero.

Next we draw the background, making use of of ShapeFactoryComp class once again. We set the background's x and y positions to zero and add it as a child of the popUpPanel.

Now it's time for our TextField. To create a bit of margin we set the TextField's x property to bg.x plus 3, and it's y property to bg.y plus 2. Keeping these margins in mind, we make the width of the TextField to be the bg.width minus 6. That's a margin of 3 on the left and the right. The height is calculated in the same fashion using bg.height minus 4. Because we will be passing different and unknown numbers into this TextField we set it's autoSize property to CENTER. This means that if there is too much text to fit in the default size it will expand equally on the left and the right. Then we set the text property to use the text we passed in as _bubbleText, and apply the TextFormat bubbleFormat. We also add this as a child of the popUpPanel.

The if statement will monitor the size of the TextField, and if it is larger than the background minus the total margin of 6, then it will adjust the size of the background and make it the width of the TextField plus 6. Then it re-centers the TextField by setting it's x property to the bg.x plus 3.

Now that everything is in place, we use Tweener to change the alpha property from 0 to 1.

[as]
public function updateSliderTxt(nt:String):void{
    ppTF.text = nt;
    ppTF.setTextFormat(bubbleFormat);
    if(ppTF.width > (bg.width - 6)){
        bg.width = ppTF.width + 6;
        ppTF.x = bg.x + 3;
    }else if(ppTF.width < (bg.width - 6)){
        bg.width = ppTF.width + 6;
        ppTF.x = bg.x + 3;
    }
}
[/as]

The public function updateSliderTxt provides a means to update the sliderBubble's text without redrawing the bubble itself. We pass in the new text value as a parameter of the function and assign it to the TextField. We set the TextFormat property again and then set about checking whether the TextField is either larger than the background, or smaller than the background and adjust things accordingly.

That's It - For Now

The XSlider has quite a bit of versatility, but there is always more you can do. Customizing this for your own needs is really quite simple. Let's say you find yourself needing a slider in a place where there is a dark background. You can alter the colors of everything - except the text. Currently the scaleFormat's color property is hard coded to be black. To make this more flexible and expose the text color property you can simply create a public variable. We'll call it txtColor and set it's default color to black. Now, in the scaleFormat, change the scaleFormat.color property to be: scaleFormat.color = txtColor;. So far nothing has changed to affect any other instance of the XSlider class you are using. But now you can set the text color because the txtColor variable is public. This is just one of many properties you can expose. Often times these classes evolve for me as specific needs arise. In a more complex modification example, let's say that you need a volume slider. You already have most of what you need, but it might be nice to use a different kind of graphic in place of the scale. We want to add a triangle of sorts that starts out thin on the left and rises up as we move right. That's pretty simple, right? Well, it is, but you need to provide a few other things as well. For instance, we'll need a Boolean property we'll call useAsVolume. This will let us know whether or not we need to draw the special volume graphic. And speaking of that, we'll need a Sprite we'll call volumeBG. This will require a new function in our ShapeFactoryComp class we could call createVolumeBG. So with just a few changes in place we have transformed our slider into a workable volume slider. 

As always, if you have any questions, just ask away!