Wednesday, December 8, 2010

Flex 4 Simple Button example

 So, I'm a programmer. I like to make reusable bits of code, called objects. These allow me to minimize the busy work I'm doing while building large complicated applications. Adobe, in some kind of pitched fever dream, decided to neuter the programmers ability to make reusable bits of code for visual elements by not providing any new kind of SimpleButton object compatible with skins.

Flex 4 introduced the concept of skins. Skins allow a designer (art people), using some other piece of Adobe software, to affect the look and feel of your buttons and whatnot without you (the programmer) having to get involved. So, as a one man band, if I want to make a button without graphics, I have nothing to worry about. I do not need a custom skin. But if I DO need custom graphics for my buttons, then it seems that Adobe wants me to make a unique skin for every, single, button.

This would add too many files to my project, making it cumbersome to support and maintain. I like the cleaner MVC/Spring Hybrid approach, so I decided to make a reusable simple button object myself. The button will take care of the skinning all by itself , and allow me to specify graphics files on the button object. The first thing I'll define is the button object. I've named it IconButton just to distinguish it from Adobe's SimpleButton. Note the imports, I'm extending spark components in order to be able to reference this in the Flash Builder visual designer, and just to show it can be done ;3.

import flash.display.Bitmap;
import flash.events.MouseEvent;

import spark.components.Button;

public class IconButton extends Button
{
 
    //  We have member variables bitmap classes for the
    //  various skin states, this allows us to specify files from
    // from the Flex 4 Visual Designer file chooser
   
    private var _bitmapUpClass:Class;
   
   
    private var _bitmapDownClass:Class;
   
   
    private var _bitmapOverClass:Class;
   
    // Note that as the classes get assigned we
    // immediatley create an instance of the class
    // for the corresponding bitmaps
   
    [Bindable]
    [Inspectable(category="General", type="Class")]
    public function set bitmapUpClass(value:Class):void{
        _bitmapUpClass = value;
        this.bitmapUp = new _bitmapUpClass();
    }
   
    [Bindable]
    [Inspectable(category="General", type="Class")]
    public function set bitmapDownClass(value:Class):void{
        _bitmapDownClass = value;
        this.bitmapDown = new _bitmapDownClass();
    }
   
    [Bindable]
    [Inspectable(category="General", type="Class")]
    public function set bitmapOverClass(value:Class):void{
        _bitmapOverClass = value;
        this.bitmapOver = new _bitmapOverClass();
    }
   
    private var _bitmapUp:Bitmap;
   
   
    private var _bitmapDown:Bitmap;
   
   
    private var _bitmapOver:Bitmap;
   
    [Bindable]
    public function set bitmapUp(value:Bitmap):void{
        _bitmapUp = value;
    }
   
    [Bindable]
    public function get bitmapUp():Bitmap{
        return _bitmapUp;
    }
   
    [Bindable]
    public function set bitmapDown(value:Bitmap):void{
        _bitmapDown = value;
    }
   
    [Bindable]
    public function get bitmapDown():Bitmap{
        return _bitmapDown;
    }
   
    [Bindable]
    public function set bitmapOver(value:Bitmap):void{
        _bitmapOver = value;
    }
   
    [Bindable]
    public function get bitmapOver():Bitmap{
        return _bitmapOver;
    }
   
    // The constructor, the call to setStyle with our custom
    // skin class is the key take away here
   
    public function IconButton()
    {
        super();
        setStyle("skinClass",com.rory.buttons.IconButtonSkin);
    }
   
}

So not a lot going on here, we take in classes that refer to bitmaps, and as they get assigned we immediately instantiate them and assign them to their corresponding bitmap member variables. We also set the style parameter "skinClass" in the constructor. Next we need to define the considerably more complicated IconButtonSkin class.

import flash.events.Event;

import mx.events.FlexEvent;
import mx.events.StateChangeEvent;
import mx.graphics.BitmapFill;
import mx.states.AddItems;
import mx.states.State;

import spark.primitives.supportClasses.GraphicElement;
import spark.skins.spark.ButtonSkin;

public class IconButtonSkin extends ButtonSkin
{
   
    //This binds the skin to our custom button object - it makes Flex happy
   
    [hostComponent("com.stthomas.edu.buttons.IconButton")]
   
    //This will allow to draw our own specified bitmaps
   
    private var _image:BitmapFill = new BitmapFill();
   
    //This will be our corporeal reference to the IconButton instance this
    //skin is tied to
   
    private var _hostIconButton:IconButton;
   
    //This is a convenience object that will help us disable the stuff we don't
    //want showing up over our lovely bitmaps.
   
    private var _stateToObjects:Object;
   
    //The constructor called by the flex framework when it feels motivated 
    //to do so. Note the listener being added to call creationCompleteListener. 
    //This is a little timing trick that makes the whole thing possible.
   
    public function IconButtonSkin()
    {
        super();         

       this.addEventListener(FlexEvent.CREATION_COMPLETE,creationCompleteListener);
    }
   
    //This is the workhorse of the whole thing.
   
    public function creationCompleteListener(event:Event){
       
        //This will intercept state changes and allow us to display our
        //own bitmaps
       
        addEventListener(StateChangeEvent.CURRENT_STATE_CHANGING, onStateChanging);
       
        //We can reference the button attached to this skin instance by referring
        //to super.hostComponent - the flex framework populated 

        //this for us assumingly with magic
       
        _hostIconButton = hostComponent as IconButton;
       
        //This sets up the initial thing we want to draw on the button, the
        //buttonUp image
       
        var widthOfBitmap:int = _hostIconButton.bitmapUp.width;
        _image.source = _hostIconButton.bitmapUp;
        _image.scaleX = 1.0;
        _image.scaleY = 1.0;
       
        //this populates the appropriately named member variable
       
        populateStateToObject();
       
        //this will strip out all of the layers of the 

        //flex button we currently don't
        //care about - in my real version of the class i have the 

        //option of leaving some of the sheen on that 
        //the default flex button provides (because it looks cool)
       
        setupSimpleButton();
       
        //some math junk that centers stuff
       
        if(widthOfBitmap< fill.width){
            fill.horizontalCenter = (( fill.width - widthOfBitmap)/2);
        }
    }
   
    //every spark object has a group of 'default' top level display elements

    //that get applied to every state, and every state has associated 'extra' 
    //display elements to apply when the spark object is in that specific
    //state. This simply sets all those elements to alpha=0.0 and ours to 1.0
   
    private function setupSimpleButton(){
        for(var prop:Object in _stateToObjects){
            var temp:Array = _stateToObjects[prop] as Array;
            for(var i:int = 0 ; i < temp.length ; i++){
                var element:GraphicElement = temp[i] as GraphicElement;
                element.alpha = 0.0;
            }
        }
        fill.alpha = 1.0;
        fill.fill = _image;
    }
   
    private function populateStateToObject(){
        stateToObjects = new Object();
        var _baseArray:Array = new Array();
       
        //gets all the 'default' objects
        for(var i:int = 0 ; i < numElements;i++){
            if(getElementAt(i) instanceof GraphicElement){
                _baseArray.push(getElementAt(i));
            }
        }
       
        //gets all the objects associated with a 'state' 


        for(var i:int = 0 ; i < states.length ; i++){
            var state:State = states[i];
            var itemsInState:Array = new Array();
            var override:Array = state.overrides;
            for(var k:int = 0 ; k < _baseArray.length ; k++){
                itemsInState.push(_baseArray[k]);
            }
            for(var j:int = 0 ; j < override.length; j++){
                var action:Object = override[j];
                if(action instanceof AddItems){
                    var addItem:AddItems = action as AddItems;
                    if(addItem.items instanceof GraphicElement && _baseArray.lastIndexOf(addItem.items,0)==-1){
                        itemsInState.push(addItem.items);
                    }
                }
            }
            _stateToObjects[state.name] = itemsInState;
        }
    }
   
    //finally we can catch a state and switch the bitmap 

    //we are displaying. We are grabbing the bitmap off 
    //of the host IconButton, this maybe breaks the line 
    //of 'visual code' and 'controller code'. But this
    //small break keeps us from having to define a new skin 

    //for every button with custom bitmaps :LSOS:?!
   
    public function onStateChanging(event:StateChangeEvent){
        switch (event.newState){
            case "down":
                _image.source = _hostIconButton.bitmapDown;
                break;
            case "over":
                _image.source = _hostIconButton.bitmapOver;
                break;
            default :
                _image.source = _hostIconButton.bitmapUp;
        }
    }
}


This skin is slightly simplified from what I use, but it should provide a functional simple button. You can do all sorts of fun things in the skin by overriding parent functions. Remember to use cntrl+click in Flex Builder to view the source (if available) of any interesting component.

If you put this in your flex project, it should show up in you visual designer component tab under the 'custom' directory.  You can drag and drop and assign your image classes in the categories section of the properties tab (under common). This simple button is really anything but, but it helps open the door into programmatic skins and well structured code :).

No comments:

Post a Comment