Creating custom item renderers with FeathersControl and IListItemRenderer, IDataGridCellRenderer, ITreeItemRenderer, or IGroupedListItemRenderer (Starling version)
The FeathersControl
class it the most basic foundation of all Feathers user interface components, including item renderers. With that in mind, if you need a custom item renderer for a List
, DataGrid
, Tree
or GroupedList
, you're actually going to create a custom Feathers component. An item renderer will have a few extra properties that are needed to communicate with its owner, but ultimately, it will be very similar to any regular Feathers component.
Feathers includes several interfaces that define the API used by the List
, DataGrid
, Tree
, or GroupedList
components to communicate with their item renderers.
IListItemRenderer
can be used to implement an item renderer forList
.IDataGridCellRenderer
can be used to implement an item renderer forDataGrid
.ITreeItemRenderer
can be used to implement an item renderer forTree
.IGroupedListItemRenderer
can be used to implement an item renderer forGroupedList
.
Additionally, GroupedList
has two more interfaces for its headers and footers.
IGroupedListHeaderRenderer
can be used to implement a header renderer forGroupedList
.IGroupedListFooterRenderer
can be used to implement a footer renderer forGroupedList
.
Below, we will look at how to create a simple custom item renderer using one of these interfaces. We'll also be taking peek at many aspects of the core architecture used by the Feathers components. At the very end, the complete source code for a simple custom item renderer will be provided to offer a starting point for other custom item renderers.
The
FeathersControl
class comes from the low-level foundation of the Feathers architecture, and it requires an intimate knowledge of Feathers internals to use effectively. You may be able to get better performance with it over the alternative, but it's a bit trickier to manage for developers that are less experienced with Feathers. If you're looking for the easiest way to built custom item renderers, please see Creating custom item renderers withLayoutGroup
instead.
The Simplest Item Renderer
Let's implement a very simple item renderer. It will contain a Label
component to display some text and it will be possible to customize some padding around the edges.
When it's finished, we'll want to use it like this:
var list:List = new List();
list.itemRendererFactory = function():IListItemRenderer
{
var itemRenderer:CustomFeathersControlItemRenderer = new CustomFeathersControlItemRenderer();
itemRenderer.padding = 10;
return itemRenderer;
};
list.dataProvider = new ArrayCollection(
[
{ label: "One" },
{ label: "Two" },
{ label: "Three" },
{ label: "Four" },
{ label: "Five" },
]);
this.addChild(list);
Notice that we set a padding
property to adjust the layout. The item renderer will get the text for its Label
sub-component from the label
property of an item in the data provider.
We could go crazy and add background skins, icons, the ability to customize the which field from the item that the label text comes from, and many more things. We're going to keep it simple for now to avoid making thing confusing with extra complexity.
For this example, we're creating an item renderer for a List
component, but it will be virtually the exact same process to create an item renderer for a Tree
or GroupedList
component (or even a header renderer or footer renderer for a GroupedList
). You simply need to change the interface that you implement. For example, instead of the IListItemRenderer
interface, you might implement the ITreeItemRenderer
or IGroupedListItemRenderer
interface.
Implementation Details
Let's start out with the basic framework for our custom item renderer. We want to subclass feathers.core.FeathersControl
and we want to implement the feathers.controls.renderers.IListItemRenderer
interface:
package
{
import feathers.controls.renderers.IListItemRenderer;
import feathers.core.FeathersControl;
public class CustomFeathersControlItemRenderer extends FeathersControl implements IListItemRenderer
{
public function CustomFeathersControlItemRenderer()
{
}
}
}
Next, we'll implement the properties required by the IListItemRenderer
interface.
Implementing IListItemRenderer
The IListItemRenderer
interface defines several properties, including owner
, index
, data
, factoryID
, and isSelected
. Each of these properties can be implemented the same way in most cases, and the relevant code is included below.
protected var _index:int = -1;
public function get index():int
{
return this._index;
}
public function set index(value:int):void
{
if(this._index == value)
{
return;
}
this._index = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
The index
refers to the item's location in the data provider. One use for this property might be to display it at the beginning of a label.
protected var _owner:List;
public function get owner():List
{
return this._owner;
}
public function set owner(value:List):void
{
if(this._owner == value)
{
return;
}
this._owner = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
Use the owner
property to access the List
component that uses this item renderer. You might use this to listen for events from the List
, such as to know when it begins scrolling.
protected var _data:Object;
public function get data():Object
{
return this._data;
}
public function set data(value:Object):void
{
if(this._data == value)
{
return;
}
this._data = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
The data
property contains the item displayed by the item renderer. The properties of this item can be used to display something in the item renderer. There are no rules for how to interpret the item's properties, but we'll show a basic example later.
protected var _factoryID:String;
public function get factoryID():String
{
return this._factoryID;
}
public function set factoryID(value:String):void
{
this._factoryID = value;
}
The factoryID
property stores the ID of the factory used by the list to create this item renderer. In general, you won't need to reference this property inside your item renderer class, but it could be used to change the item renderer's behavior or appearance if the same class were used in multiple factories. The list uses this property internally to manage its item renderers.
protected var _isSelected:Boolean;
public function get isSelected():Boolean
{
return this._isSelected;
}
public function set isSelected(value:Boolean):void
{
if(this._isSelected == value)
{
return;
}
this._isSelected = value;
this.invalidate(INVALIDATION_FLAG_SELECTED);
this.dispatchEventWith(Event.CHANGE);
}
The isSelected
property indicates if the item has been selected. It's common for an item to be selected when it is touched, but that's not required.
The
ITreeItemRenderer
andIGroupedListItemRenderer
interfaces are very similar. Instead of anindex
property, this type of item renderer has different properties to specify where in the hierarchical data provider the item is located.Tree
has alocation
property, andGroupedList
hasgroupIndex
anditemIndex
properties. An additionallayoutIndex
property specifies the item's order in the layout. Theowner
property should be typed asTree
orGroupedList
instead ofList
, obviously.
Header and footer renderers in a
GroupedList
are similar to item renderers in aGroupedList
. See theIGroupedListHeaderRenderer
andIGroupedListFooterRenderer
interfaces. These renderers have agroupIndex
and alayoutIndex
, but noitemIndex
.
Adding Children
We want to display a Label
component, so let's add a member variable for it:
protected var _label:Label;
Next, we want to create a new instance and add it as a child. We need to override initialize()
function:
override protected function initialize():void
{
this._label = new Label();
this.addChild(this._label);
}
The initialize()
function is called once the very first time that the component is added to the stage. It's a good place to create sub-components and other children and possibly to do things like add event listeners that you don't intend to remove until the component is disposed. In general, it is better to use initialize()
for this sort of thing instead of the constructor.
For more information about the
initialize()
function and other parts of the Feathers architecture, see Anatomy of a Feathers Component.
Parsing the data
Next, we want to access the data
property and display something in our Label
component. Let's start by overriding the draw()
function and checking if the appropriate invalidation flag is set to indicate that the data has changed.
override protected function draw():void
{
var dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA);
if(dataInvalid)
{
this.commitData();
}
}
You may remember that we called the invalidate()
function in the setter functions above. In the data
setter, we passed in INVALIDATION_FLAG_DATA
. Inside the draw()
function, we call isInvalid()
to see if that flag has been set.
For more information about the
draw()
function and other parts of the Feathers architecture, see Anatomy of a Feathers Component.
Let's add a commitData()
function to call when the data changes:
protected function commitData():void
{
if(this._data)
{
this._label.text = this._data.label;
}
else
{
this._label.text = null;
}
}
For this particular item renderer, we're requiring all items in the data provider to have a label
property that holds the text to display in the Label
component. If we were building a generic item renderer, ideally, we might like to make that field name customizable, like the labelField
property in DefaultListItemRenderer
. However, let's keep it simple.
Don't forget to handle the case where the data property is null
. You don't want any runtime errors causing you trouble.
Measuring the Item Renderer
Next, we want the item renderer to be able to measure itself if its width and height property haven't been set another way. Before we get to that, let's add that padding
property that we used in the example code above to add some extra space around the edges of the Label
component:
protected var _padding:Number = 0;
public function get padding():Number
{
return this._padding;
}
public function set padding(value:Number):void
{
if(this._padding == value)
{
return;
}
this._padding = value;
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
With that in place, let's add an autoSizeIfNeeded()
function. This isn't something that's built into Feathers, but most of the core Feathers components have a function like this because it's a nice consistent place for a component to measure itself. To keep things easy to digest, we'll break it up into a few parts:
protected function autoSizeIfNeeded():Boolean
{
var needsWidth:Boolean = isNaN(this.explicitWidth);
var needsHeight:Boolean = isNaN(this.explicitHeight);
if(!needsWidth && !needsHeight)
{
return false;
}
Let's start by checking whether the width and height properties have been set. We have internal variables named explicitWidth
and explicitHeight
that will either be a valid number of pixels or they will be NaN
if they aren't set. If both the width and the height have been set already, we can simply return without any measuring.
For more information about the
explicitWidth
andexplicitHeight
variables, and other parts of the Feathers architecture, see Anatomy of a Feathers Component.
Next, we update the width and height of the Label
sub-component. If the item renderer's explicit dimensions are NaN
, then the explicit dimensions of the Label
will be set to NaN
too, meaning that the Label
should measure itself too, just like the item renderer is doing.
this._label.width = this.explicitWidth - 2 * this._padding;
this._label.height = this.explicitHeight - 2 * this._padding;
this._label.validate();
Next, we want to use the width and height values from the Label
to calculate the item renderer's final width and height:
var newWidth:Number = this.explicitWidth;
if(needsWidth)
{
newWidth = this._label.width + 2 * this._padding;
}
var newHeight:Number = this.explicitHeight;
if(needsHeight)
{
newHeight = this._label.height + 2 * this._padding;
}
In more complex item renderers, we might add together the dimensions of multiple sub-components. For this simple item renderer, we'll simply ask the Label
sub-component for its width and height, and then we add the padding to those values.
Finally, we tell Feathers what the final dimensions will be using the saveMeasurements()
function:
return this.saveMeasurements(newWidth, newHeight, newWidth, newHeight);
The return value is true if the dimensions are different than the last time that the component validated. We return this same value from autoSizeIfNeeded()
for use in the draw()
function.
For more information about the
saveMeasurements()
function and other parts of the Feathers architecture, see Anatomy of a Feathers Component.
Speaking of the draw()
function, we want to add some code to call the autoSizeIfNeeded()
from there:
override protected function draw():void
{
var dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA);
if(dataInvalid)
{
this.commitData();
}
this.autoSizeIfNeeded();
}
Notice that we don't actually use the returned Boolean
value. This particular component doesn't need it, but more complex components may use that value, along with INVALIDATION_FLAG_SIZE
, to selectively call other functions.
Adjusting the layout
We now have the final dimensions of the item renderer, so let's position and size the Label
sub-component. Let's do that in a new layoutChildren()
function:
protected function layoutChildren():void
{
this._label.x = this._padding;
this._label.y = this._padding;
this._label.width = this.actualWidth - 2 * this._padding;
this._label.height = this.actualHeight - 2 * this._padding;
}
The actualWidth
and actualHeight
variables hold the final width and height of the item renderer. These variables are derived using a combination of the explicit dimensions and the measured dimensions that we calculated before passing them to saveMeasurements()
.
For more information about the
actualWidth
andactualHeight
variables, and other parts of the Feathers architecture, see Anatomy of a Feathers Component.
We call the layoutChildren()
function at the end of the draw()
function:
override protected function draw():void
{
var dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA);
if(dataInvalid)
{
this.commitData();
}
this.autoSizeIfNeeded();
this.layoutChildren();
}
Source Code
The complete source code for the CustomFeathersControlItemRenderer
class is included below:
package
{
import feathers.controls.Label;
import feathers.controls.List;
import feathers.controls.renderers.IListItemRenderer;
import feathers.core.FeathersControl;
import starling.events.Event;
public class CustomFeathersControlItemRenderer extends FeathersControl implements IListItemRenderer
{
public function CustomFeathersControlItemRenderer()
{
}
protected var _label:Label;
protected var _index:int = -1;
public function get index():int
{
return this._index;
}
public function set index(value:int):void
{
if(this._index == value)
{
return;
}
this._index = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
protected var _owner:List;
public function get owner():List
{
return this._owner;
}
public function set owner(value:List):void
{
if(this._owner == value)
{
return;
}
this._owner = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
protected var _data:Object;
public function get data():Object
{
return this._data;
}
public function set data(value:Object):void
{
if(this._data == value)
{
return;
}
this._data = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
protected var _factoryID:String;
public function get factoryID():String
{
return this._factoryID;
}
public function set factoryID(value:String):void
{
this._factoryID = value;
}
protected var _isSelected:Boolean;
public function get isSelected():Boolean
{
return this._isSelected;
}
public function set isSelected(value:Boolean):void
{
if(this._isSelected == value)
{
return;
}
this._isSelected = value;
this.invalidate(INVALIDATION_FLAG_SELECTED);
this.dispatchEventWith(Event.CHANGE);
}
protected var _padding:Number = 0;
public function get padding():Number
{
return this._padding;
}
public function set padding(value:Number):void
{
if(this._padding == value)
{
return;
}
this._padding = value;
this.invalidate(INVALIDATION_FLAG_LAYOUT);
}
override protected function initialize():void
{
if(!this._label)
{
this._label = new Label();
this.addChild(this._label);
}
}
override protected function draw():void
{
var dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA);
if(dataInvalid)
{
this.commitData();
}
this.autoSizeIfNeeded();
this.layoutChildren();
}
protected function autoSizeIfNeeded():Boolean
{
var needsWidth:Boolean = isNaN(this.explicitWidth);
var needsHeight:Boolean = isNaN(this.explicitHeight);
if(!needsWidth && !needsHeight)
{
return false;
}
this._label.width = this.explicitWidth - 2 * this._padding;
this._label.height = this.explicitHeight - 2 * this._padding;
this._label.validate();
var newWidth:Number = this.explicitWidth;
if(needsWidth)
{
newWidth = this._label.width + 2 * this._padding;
}
var newHeight:Number = this.explicitHeight;
if(needsHeight)
{
newHeight = this._label.height + 2 * this._padding;
}
return this.saveMeasurements(newWidth, newHeight, newWidth, newHeight);
}
protected function commitData():void
{
if(this._data)
{
this._label.text = this._data.label;
}
else
{
this._label.text = null;
}
}
protected function layoutChildren():void
{
this._label.x = this._padding;
this._label.y = this._padding;
this._label.width = this.actualWidth - 2 * this._padding;
this._label.height = this.actualHeight - 2 * this._padding;
}
}
}
Next Steps
Looking to do more with your custom item renderer? Check out the Feathers Cookbook for "recipes" that show you how to implement typical features in custom item renderers.
Related Links
feathers.controls.renderers.IListItemRenderer
API Documentationfeathers.controls.renderers.IDataGridCellRenderer
API Documentationfeathers.controls.renderers.ITreeItemRenderer
API Documentationfeathers.controls.renderers.IGroupedListItemRenderer
API Documentationfeathers.controls.renderers.IGroupedListHeaderOrFooterRenderer
API Documentation