Playing with Feathers UI Components for Starling Framework
The Starling Framework allows developers to build 2D hardware accelerated (Stage3D API) games with an API similar to the flash.display.* classes. This framework is used by Angry Birds on Facebook. The Starling community is quite vibrant and many extensions are being made for it. One of these extensions that has grown into a full project by it self is called Feathers UI (previously known as Foxhole UI components), developed by Josh Tynjala.
The feathersui.com website provides great documentation, api docs, and examples of how to get started with the Feathers UI components.
For myself there is no better way to learn something new than using it in a project. I wanted to build out a basic file inspector application and use it to play with the Feathers UI components. It took a bit to figure out what I wanted but the Feathers UI components should feel pretty close to non-mxml Flex or QNX UI. Feathers controls have a defined lifecycle and an approach to theme your components globally. The example apps and wiki documentation is a great place to start putting the pieces together.
Once I was a bit comfortable with the basics I started addressing what I wanted in my application. It quickly turned to wanting percent based width and height for laying out a group of controls. Basically a VGroup or HGroup in Flex. The Feathers ScrollContainer
and other controls do support a layout property that handles the layout of their children. So I was torn between trying to extend ScrollContainer
or just make a control of my own. I went with building my own and worked hard to use the Feathers UI lifecycle (initialize, draw, invalidate, etc…). This way I didn’t have to worry about breaking ScrollContainer
logic and focus on what I wanted the control to do while creating my first custom Feathers UI component.
I wanted a VGroup
type component, but with the ability to set certain children with percentage based height. I didn’t want to add percentWidth
and percentHeight
properties to FeathersControl
, which all components inherit. This would allow others to try my custom control without a custom Feathers UI fork. I also didn’t want to force adherence to the layout property API because of my limited time to do a prototype, so all the layout is inherited in the VGroup
. I did end up extending out a Group
class and reusing parts for an HGroup
as well, but the vertical and horizontal layout differences are in the VGroup
and HGroup
draw()
function calls respectively.
Let us look at some code, you can find all custom Feathers UI components on my Github in the RenaunsCoreLib repo:
com.renaun.controls.Group
{
import feathers.controls.Label;
import feathers.core.FeathersControl;
import starling.display.DisplayObject;
public class Group extends FeathersControl
{
public function Group()
{
super();
}
protected var children:Vector.<FeathersControl> = new Vector.<FeathersControl>();
protected var childrenPercentValues:Vector.<int> = new Vector.<int>();
protected var explicitTotal:Number;
protected var percentTotalValue:Number;
protected var isValid:Boolean = false;
protected var isFirstTime:Boolean = false;
/**
* Currently you have to add items in order, there is not addLayoutItemAt.
*
* @param item The target item you want to add to this group, add in order
* @param percentHeight The percent value of the VGroup's height. 100 = 100%, and if the total percent is > 100 it will be scaled by the value/totalPrecents.
*/
public function addLayoutItem(item:FeathersControl, percentValue:int = -1):void
{
addChild(item);
children.push(item);
item.onResize.add(resizeHandler);
childrenPercentValues.push(percentValue);
measure();
invalidate();
}
override protected function initialize():void
{
//trace("init");
super.initialize();
isFirstTime = true;
}
protected function measure():void
{
}
protected function resizeHandler(target:FeathersControl, oldWidth:Number, oldHeight:Number):void
{
/*
if (target is Label)
trace("resizeHandler2["+(target as Label).text+"]: " + oldWidth + " - " + width);
else
trace("resizeHandler2["+target+"]: " + oldWidth + " - " + width);
*/
isValid = false;
invalidate();
}
}
}
In the base Group
class you see the addLayoutItem
function which takes the display object reference and a percent value as parameters. It assumes you are going to add the display objects in order and define which ones are to have percentage based values and which ones are not. If you do not give it a percent value it assumes it has its height already explicitly set. If the total percent value is greater then 100, then I just divy up the actual heights as: percent value / total percent.
Note: It is not fully flushed out API but more of an experiment, for example I would add an addLayoutItemAt
function and maybe other ways to tweak values after they are added.
com.renaun.controls.VGroup
{
import feathers.core.FeathersControl;
import starling.display.DisplayObject;
public class VGroup extends Group
{
public function VGroup()
{
super();
}
public var gap:int = 8;
public var paddingLeft:int = 8;
public var paddingRight:int = 8;
public var paddingTop:int = 8;
public var paddingBottom:int = 8;
override protected function measure():void
{
var item:DisplayObject;
explicitTotal = -gap;
percentTotalValue = 0;
var i:int;
for (i = 0; i < children.length; i++)
{
item = children[i];
if (childrenPercentValues[i] > 0)
percentTotalValue += childrenPercentValues[i];
else
{
if (item is FeathersControl)
{
//item.height = (item as BitmapFontTextRenderer).measureText().y;
(item as FeathersControl).validate();
}
explicitTotal = explicitTotal + item.height;
}
explicitTotal += gap;
}
isValid = true;
}
override protected function draw():void
{
// Delay items width/height are still not valid inside initalize method so have to wait for draw
if (isFirstTime)
{
measure();
isFirstTime = false;
}
var availableHeightForPercents:Number = explicitHeight - explicitTotal - paddingTop - paddingBottom;
//trace("availableHeightForPercents: " + availableHeightForPercents + " - " + explicitTotal + " - " + explicitHeight);
// Make smaller if the percents add up more then 100
if (percentTotalValue > 100)
availableHeightForPercents = availableHeightForPercents / (percentTotalValue / 100);
//trace("percentTotalValue: " + percentTotalValue + " aHFP: " + availableHeightForPercents + " - " + explicitTotal + " - " + explicitHeight);
var percentHeightValue:int = 0;
var lastHeight:int = 0;
var i:int;
var item:DisplayObject;
for (i = 0; i < children.length; i++)
{
item = children[i];
// Set Y
item.y = lastHeight + paddingTop;
if (childrenPercentValues[i] > 0)
{
percentHeightValue = childrenPercentValues[i];
// Set HEIGHT
item.height = int(availableHeightForPercents * percentHeightValue / 100);
}
lastHeight += item.height + gap;
// Set X
item.x = paddingLeft;
// Set WIDTH
item.width = explicitWidth - paddingLeft - paddingRight;
//trace("lastHeight : " + lastHeight);
}
}
}
}
In the actual layout code implemented inside the draw()
function I proceed first to get the total explicit height values of non-percent based layout items that were added. Then I take the remainder height and divy it up between the percent based items. Fairly straightforward and quite easy to create my own custom Feathers UI control.
Using the VGroup
looks like this:
vgroup.addLayoutItem(renderer);
vgroup.addLayoutItem(list, 100);
vgroup.addLayoutItem(appCountLabel);
addChild(vgroup);
My components are not vetted for all bugs, API completeness, or performance issues as this was just my way getting to understand the Feathers UI components better. I did enjoy using the UI components and did not find it hard to extend and play with. I do need to thank Josh for answering my questions; he does a great job responding to the Feathers UI forums.
You can also check out the relative position manager class I made for fun. It supports left, right, top, bottom, horizontalCenter and verticalCenter.