Using QNX Image and ImageCache in List Custom CellRenderer
In this blog I’ll show how to create a custom cell renderer skin for the QNX UI component List
class. The custom cell renderer will display an image, text, and box beneath the text for affect. The List
class implements virtualization of the cell renderers and reuses instances of the objects that are displayed as you scroll up and down. This allows for memory optimization and speed gains by not creating and destroying a bunch of cell renderer instances. In the process of displaying images in virtualizated instances you end up changing the source each time the data is assigned to the cell renderer instance.
This causes a problem of performance and visual quirks. For example if updating the image url takes longer then the time it takes to have the data value changed with a new value (try flicking a long list really fast, the instances of the cell renderer will be set a bunch of times before it slows down). This situation might cause a flicker of the old image before the new image loads. Luckily QNX classes probably a mechanism to help with this situation.
By caching the loaded images after the first load you can quickly load image data before any new data is set in a cell renderer. I will now describe in detail how this works with some code. All of the code below can be found on github at https://github.com/renaun/QNXListImageCache.
Example Application Screenshot
Getting Data
This example application pulls down the Flickr image feed for the Adobe MAX flickr account. It parses the RSS feed in rss2 format and creates a QNX DataProvider
class with data objects holding the title and url to the image.
URLLoader
Use the URLLoader
class to load the flickr feed and then listen for the Event.COMPLETE
event to parse the data.
{
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onLoadData);
// Loads a list of MAX images returned as json format
loader.load(new URLRequest("http://api.flickr.com/services/feeds/photos_public.gne?id=43067655@N07&format=rss2"));
}
XML & DataProvider
The flickr feed was requested to be in the rss2 format. We use the flickr “media” specific namespace to pull of the title and content from the rss2 items.
{
var xml:XML = new XML(event.target.data);
var media:Namespace = new Namespace("http://search.yahoo.com/mrss/");
var xmlList:XMLList = xml..item;
var len:int = xmlList.length();
for (var i:int = 0; i<len; i++)
{
var obj:Object = new Object();
obj.text = xmlList[i].media::title+"";
obj.url = xmlList[i].media::content.@url + "";
dp.addItem({label:obj.text, data:obj});
}
// ... more code here
}
List & Label
Now we continue in the onLoadData
function and create the List
and Label
for the application.
{
// ... previous code here
list = new List();
list.setSkin(CacheImageCellRenderer);
list.dataProvider = dp;
list.width = stage.stageWidth;
list.height = stage.stageHeight;
list.columnWidth = 240;
list.rowHeight = 240;
list.x = 0;
list.y = 60;
list.addEventListener(ListEvent.ITEM_CLICKED, itemClickedHandler);
addChild(list);
var format:TextFormat = new TextFormat();
format.size = 24;
format.bold = true;
format.color = 0xFFFFFF;
lblSelected = new TextField();
lblSelected.defaultTextFormat = format;
lblSelected.x = 6;
lblSelected.y = 6;
lblSelected.text = "Selected an image!";
addChild(lblSelected);
layout();
}
Taking a look at the List class you will see list.setSkin(CacheImageCellRenderer);
and list.dataProvider = dp;
. Now any time the List
class makes a new cell renderer object instance it will create a new com.renaun.renderer.CacheImageCellRenderer
instance. As the List
scrolls up and down each cell renderer instance will be assigned with data from the list.dataProvider
which we set with the dp
value.
com.renaun.renderer.CacheImageCellRenderer
The custom cell renderer in this example extends QNX’s CellRenderer and overrides the data setter function. The class prepares the Image
, Label
, and background graphic components in the constructor. It doesn’t set any parts of the components that depend on data values, that is left up to the updateCell() method which is called after a new data value is set. Here is the whole class, we’ll take a look at the parts below:
{
import flash.display.Sprite;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;
import qnx.ui.display.Image;
import qnx.ui.listClasses.CellRenderer;
import qnx.ui.text.Label;
import qnx.utils.ImageCache;
public class CacheImageCellRenderer extends CellRenderer
{
/**
* This is a static cache object. Alternatively you can
* create this object out side this class to be used
* in more then one place.
*/
public static var cacheObject:ImageCache = new ImageCache();
public function CacheImageCellRenderer()
{
super();
initBackground();
}
/**
*
*/
protected var img:Image;
protected var lbl:Label;
protected var bg:Sprite;
protected var format:TextFormat;
/**
* Updates the text and image everytime a new data is set
* for this renderer instance.
*/
override public function set data(value:Object):void
{
super.data = value.data;
//trace(value.data.text + " - " + value.data.url);
updateCell();
}
/**
* Update the image and text if there is valid data.
*/
private function updateCell():void
{
if (this.data)
{
//trace("loading: " + data.url);
img.setImage(data.url);
lbl.text = data.text;
}
}
/**
* Create all the cell renderer components just once
*/
private function initBackground():void
{
img = new Image();
lbl = new Label();
bg = new Sprite();
format = new TextFormat();
format.color = 0xEEEEEE;
format.size = 12;
format.bold = true;
bg.graphics.clear();
bg.graphics.beginFill(0x000000, .7);
bg.graphics.drawRect(0, 0, 240, 50);
bg.graphics.endFill();
img.setPosition(0,0);
img.setSize(240,134);
// Setting the cache propery on QNX Image class
// takes care of adding and checking for images on
// the ImageCache object.
img.cache = CacheImageCellRenderer.cacheObject;
lbl.width = 240;
lbl.format = format;
lbl.wordWrap = true;
lbl.x = 5;
lbl.y = 0;
lbl.width = 220;
lbl.autoSize = TextFieldAutoSize.LEFT;
addChild(img);
addChild(bg);
addChild(lbl);
}
}
}
updateCell()
Each time that a new data object is set to this instance it updates its appearance by setting the img.setImage(data.url);
and lbl.text = data.text;
.
ImageCache
The class has a static instance of ImageCache
class called cacheObject
. This static reference could have been outside of this class but for this example the main point is to have the same instance of the ImageCache
class ready to assign to X number of Image
class instances.
img.cache
For each instance of the custom cell renderer there is a QNX Image
class created which will change based on the data.url
value. By setting img.cache = CacheImageCellRenderer.cacheObject;
we are telling the Image class to first check for the image in the CacheImageCellRenderer.cacheObject
instance. If the image source, keyed by the url, is not found it will load the image data and then save it in the CacheImageCellRenderer.cacheObject
instance. This makes subsequent or frequent calls to load image data in the Image
class very fast as it pulls the cached BitmapData
from CacheImageCellRenderer.cacheObject
.
There isn’t really much to it, this is how to get great performance by using the ImageCache
with your Image components in your custom List cell renderer
Where to go from here
The ImageCache
will use more memory. You can limit the number of items the ImageCache
class caches, get out the api here. You can also use one static reference to one instance of ImageCache
across different components, depending on the content of an application this might make sense to do.
Another approach to bypass the initial loading times as the List first appears is to actual prefill the ImageCache
instance with the initial images.
Pingback: links for 2011-01-14 « Boskabout
Pingback: Extending QNX TileList: Liquid Tile List : Mihai Corlan
Pingback: Extending QNX TileList: Liquid Tile List (Adobe Flash Platform Blog)