Disclaimer: I’m fairly certain this isn’t best practice. There is a ton of stuff going on with List
and CellRenderer
that I don’t fully understand. I’m hoping to sit down with the QNX team at some point and figure out how this might be done correctly, but for now, I’ve found a way that works so I wanted to share it for anyone who is having issues.
Skinning a List
is quite a bit different than skinning something simple like a Button
or a TextInput
box (which I talked about in my post here). It seems like the correct way to customize the look and feel of a list would be to create a CellRenderer
which gives you access to the label for the list, and then swap out the graphics by creating a skin specifically for that CellRenderer
. But my particular code didn’t rely on the label field so I skipped the first step and just created a CellRendererSkin
that extends UISkin
and implements ICellRenderer
. By implementing ICellRenderer
, I get access to the data methods, but as you’ll see, that created some issues.
First off, here’s the list I wanted to create. It’s got an image and a few lines of text. You can see the regular state (white) and what it looks like when it’s selected (grey). I wanted a little bit more of a custom look so I put some space between each item and drew a rounded rectangle with a border around the content.
To skin a list this way, the most important method is still initializeStates()
. That method has to be overridden just like if we were skinning any other component, and it’s where we call setSkinState
to associate a graphic with a state. A list has basically 8 different SkinStates
: SkinStates.UP
, SkinStates.UP_ODD
, SkinStates.DOWN
, SkinStates.FOCUS
, SkinStates.DISABLED
, SkinStates.SELECTED
, SkinStates.DOWN_SELECTED
and SkinStates.DISABLED_SELECTED
. I have no idea what FOCUS
does, I’m not alternating my rows so I don’t care about ODD, and I decided I could live with my DOWN
, SELECTED
and DOWN_SELECTED
being the same. So here’s the code for my initializeStates()
method:
override protected function initializeStates():void
{
_upSkin = new Sprite();
setSkinState(SkinStates.UP,_upSkin);
_downSkin = new Sprite();
setSkinState(SkinStates.DOWN,_downSkin);
setSkinState(SkinStates.DOWN_SELECTED,_downSkin);
setSkinState(SkinStates.SELECTED,_downSkin);
SkinStates
showSkin(_upSkin);
}
Here’s where things get a bit messier. With normal skinning, I could just start adding graphics to my sprites and then set the skin state accordingly. But what I found was that when the initializeStates()
method got called, the width and height of the component hadn’t been set yet. So when I tried to draw a rectangle that used the height/width of the component, it would look scrunched. If you know the exact dimensions you want, you can just hard-code the values. But I wanted to be able to use this on different sized lists, so I wanted those dynamic values.
What I found was that if I overrode the setState(state:String)
method, I could get the values for width/height there and then draw the correct graphics depending on whatever state was being set based on the height/width of each cell.
override protected function setState(state:String):void
{
super.setState(state);
var matrix:Matrix = new Matrix();
matrix.createGradientBox(width,height,90/180*Math.PI);
if(state == SkinStates.UP)
{
_background.graphics.clear();
_background.graphics.beginGradientFill(GradientType.LINEAR,
[0xffffff,0xf2f2f2,0xffffff],[1,1,1],[0,127,255],matrix);
_background.graphics.lineStyle(2,0x221206);
_background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
_background.graphics.endFill();
}
if(state == SkinStates.DOWN ||
state == SkinStates.DOWN_SELECTED ||
state == SkinStates.SELECTED)
{
_background.graphics.clear();
_background.graphics.beginGradientFill(GradientType.LINEAR,
[0xaaaaaa,0xcfcfcf,0xaaaaaa],[1,1,1],[0,127,255],matrix);
_background.graphics.lineStyle(2,0x221206);
_background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
_background.graphics.endFill();
}
}
Here, I ran into another odd issue. Notice that I’m not adding the graphics to the state, but rather adding them to a _background
Sprite. What I found is that when I would try to draw the graphics right on the skin sprite, it would either overwrite my other content when the state changed and I couldn’t get it back, or the graphics wouldn’t display at all. I’m still not entirely sure why those two things happened and I went through so many iterations that I don’t remember the code that caused it. But what I found was that if I created a background Sprite and added that to the display list first, I could alter it depending on the state and it would draw correctly. That’s also why I have a _background.graphics.clear()
call because the _background
Sprite is in every state, it just needs to be redrawn when the state changes.
The next step was to add everything to the cell renderer and then to clean it up when the cell renderer goes away. All of the QNX components have an onAdded()
and onRemoved()
method that gets called when the object is added to or removed from the stage. So I just overrode those methods and added my content.
override protected function onAdded():void
{
super.onAdded();
addChild(_background);
addChild(_image);
addChild(_name);
addChild(_brewery);
addChild(_beerType);
addChild(_ratingText);
addChild(_avgRating);
}
override protected function onRemoved():void
{
super.onRemoved();
removeChild(_background);
removeChild(_image);
removeChild(_name);
removeChild(_brewery);
removeChild(_beerType);
removeChild(_ratingText);
removeChild(_avgRating);
}
There’s just one final step. As you’ll see in my code below, I set up most of the properties of the labels and the image in the constructor. But I don’t set any of the dynamic data. That’s because I had a really hard time finding out when I could access the data
property of the cell renderer. There isn’t any data on init()
, onAdded()
or initializeStates()
so trying to set the dynamic data in those methods threw an error. I could access it in the setState()
method, but I found that when I tried to set it there, the list wouldn’t display the values correctly. I finally figured out that it was because of the way the list is virtualized. The setState()
method doesn’t get called when you initially scroll the list because the state hasn’t changed, just the data has. So I was seeing the values repeat when I’d scroll the list and didn’t see the correct value until I clicked on it and forced setState()
to be called.
The solution was just to embrace the fact that I was implementing ICellRenderer
and set all of the data in the data setter method. That set the data correctly for each cell and didn’t depend on the state at all. I also had to set the width of the Label
objects there so that the text wouldn’t be cut off.
public function set data(data:Object):void
{
_beer = data;
_image.setImage(data.thumb);
_name.text = data.beerName;
_name.width = width-150;
_brewery.text = data.brewerName;
_brewery.width = width-150;
_beerType.text = data.styleName;
_beerType.width = width-150;
if(data.avgRating > 3)
{
_avgRatingFormat.color = 0x4c9d17;
} else if (data.avgRating < 1)
{
_avgRatingFormat.color = 0x9d1717;
}
_avgRating.text = data.avgRating;
_avgRating.format = _avgRatingFormat;
}
Again, I want to stress that this probably isn’t the ideal way to do this. Especially if you have a pretty basic label you could just extend CellRenderer
and override the draw()
and drawLabel()
method to do what you want. Or, like I said above, apply a special skin to that CellRenderer
that handles all of the states for the list correctly. This was the rabbit hole that I went down though, and I found it to be kind of handy because I killed all of my birds with one class. Even if it’s an ugly class. Here’s the full code.
package com.pintley.components.listClasses
{
import flash.display.GradientType;
import flash.display.Sprite;
import flash.filters.DropShadowFilter;
import flash.geom.Matrix;
import flash.text.TextFormat;
import qnx.ui.display.Image;
import qnx.ui.listClasses.ICellRenderer;
import qnx.ui.skins.SkinStates;
import qnx.ui.skins.UISkin;
import qnx.ui.text.Label;
public class BeerCellRendererSkin extends UISkin implements ICellRenderer
{
protected var _beer:Object;
private var _row:int;
private var _column:int;
private var _section:int;
private var _index:int;
private var _yOffset:int = 12;
private var _xOffset:int = 35;
protected var _upSkin:Sprite;
protected var _selectedSkin:Sprite;
protected var _downSkin:Sprite;
protected var _disabledSkin:Sprite;
protected var _background:Sprite;
protected var _name:Label;
protected var _brewery:Label;
protected var _beerType:Label;
protected var _ratingText:Label;
protected var _avgRating:Label;
protected var _image:Image;
protected var _nameFormat:TextFormat;
protected var _breweryFormat:TextFormat;
protected var _beerTypeFormat:TextFormat;
protected var _ratingTextFormat:TextFormat;
protected var _avgRatingFormat:TextFormat;
public function BeerCellRendererSkin()
{
super();
_nameFormat = new TextFormat();
_nameFormat.color = 0xbd5251;
_nameFormat.size = 16;
_nameFormat.bold = true;
_breweryFormat = new TextFormat();
_breweryFormat.color = 0x525252;
_breweryFormat.size = 14;
_breweryFormat.bold = true;
_beerTypeFormat = new TextFormat();
_beerTypeFormat.color = 0x525252;
_beerTypeFormat.size = 14;
_ratingTextFormat = new TextFormat();
_ratingTextFormat.color = 0x79523e;
_ratingTextFormat.size = 12;
_ratingTextFormat.bold = true;
_avgRatingFormat = new TextFormat();
_avgRatingFormat.color = 0x000000;
_avgRatingFormat.size = 14;
_avgRatingFormat.bold = true;
_image = new Image();
_image.x = _xOffset;
_image.y = 20;
_image.filters = [new DropShadowFilter(3,45,0x000000,.5,4,4,.5)];
_name = new Label();
_name.x = _xOffset+80;
_name.y = _yOffset;
_name.format = _nameFormat;
_brewery = new Label();
_brewery.x = _xOffset+80;
_brewery.y = _yOffset+20;
_brewery.format = _breweryFormat;
_beerType = new Label();
_beerType.x = _xOffset+80;
_beerType.y = _yOffset+35;
_beerType.format = _beerTypeFormat;
_ratingText = new Label();
_ratingText.x = _xOffset+80;
_ratingText.y = _yOffset+55;
_ratingText.format = _ratingTextFormat;
_ratingText.text = "AVG Rating";
_avgRating = new Label();
_avgRating.x = _xOffset + 150;
_avgRating.y = _yOffset+54;
_background = new Sprite();
}
public function get data():Object
{
return _beer;
}
public function set data(data:Object):void
{
_beer = data;
_image.setImage(data.thumb);
_name.text = data.beerName;
_name.width = width-150;
_brewery.text = data.brewerName;
_brewery.width = width-150;
_beerType.text = data.styleName;
_beerType.width = width-150;
if(data.avgRating > 3)
{
_avgRatingFormat.color = 0x4c9d17;
} else if (data.avgRating < 1)
{
_avgRatingFormat.color = 0x9d1717;
}
_avgRating.text = data.avgRating;
_avgRating.format = _avgRatingFormat;
}
public function get index():int
{
return _index;
}
public function set index(value:int):void
{
_index = value;
}
public function get row():int
{
return _row;
}
public function set row(value:int):void
{
_row = value;
}
public function get column():int
{
return _column;
}
public function set column(value:int):void
{
_column = value;
}
public function get section():int
{
return _section;
}
public function set section(section:int):void
{
_section = section;
}
public function get isHeader():Boolean
{
return false;
}
override protected function initializeStates():void
{
_upSkin = new Sprite();
setSkinState(SkinStates.UP,_upSkin);
_downSkin = new Sprite();
setSkinState(SkinStates.DOWN,_downSkin);
setSkinState(SkinStates.DOWN_SELECTED,_downSkin);
setSkinState(SkinStates.SELECTED,_downSkin);
showSkin(_upSkin);
}
override protected function setState(state:String):void
{
super.setState(state);
var matrix:Matrix = new Matrix();
matrix.createGradientBox(width,height,90/180*Math.PI);
if(state == SkinStates.UP)
{
_background.graphics.clear();
_background.graphics.beginGradientFill(GradientType.LINEAR,
[0xffffff,0xf2f2f2,0xffffff],[1,1,1],[0,127,255],matrix);
_background.graphics.lineStyle(2,0x221206);
_background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
_background.graphics.endFill();
}
if(state == SkinStates.DOWN ||
state == SkinStates.DOWN_SELECTED ||
state == SkinStates.SELECTED)
{
_background.graphics.clear();
_background.graphics.beginGradientFill(GradientType.LINEAR,
[0xaaaaaa,0xcfcfcf,0xaaaaaa],[1,1,1],[0,127,255],matrix);
_background.graphics.lineStyle(2,0x221206);
_background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
_background.graphics.endFill();
}
}
override protected function onAdded():void
{
super.onAdded();
addChild(_background);
addChild(_image);
addChild(_name);
addChild(_brewery);
addChild(_beerType);
addChild(_ratingText);
addChild(_avgRating);
}
override protected function onRemoved():void
{
super.onRemoved();
removeChild(_background);
removeChild(_image);
removeChild(_name);
removeChild(_brewery);
removeChild(_beerType);
removeChild(_ratingText);
removeChild(_avgRating);
}
}
}
Related Posts
- Setting Custom Labels on Lists with the PlayBook
- Skinning PlayBook Components
- Using the Container Classes to Lay Out PlayBook Applications
- Getting Started with the BlackBerry PlayBook and Adobe AIR
- The Camera API and Geolocation Exif Data on AIR for Android
History
- 18th April, 2011: Initial version