Mobile ItemRenderer in ActionScript (Part 4)
This is part 4 of my series on item renderers. This time we have a more real example that includes two states (selected and up) plus and avatar image and a bunch of text fields.
What I choose as the example is a TweetRenderer that shows the picture of the user, the user name and the content of the tweet.
For reference these are the previous posts: part 1, part 2, and part 3.
To simplify the code, I call the twitter service with a hardcoded value "Adobe" to receive all the tweets that include that word. As you may notice, I'm not following the best practices with services and added them in the Application file.
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
creationComplete="creationCompleteHandler()">
<fx:Style source="styles/Main.css"/>
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.events.FlexEvent;
[Bindable]
private var ac:ArrayCollection = new ArrayCollection();
private var searchURL:String = "http://search.twitter.com/search.atom?q=";
protected function creationCompleteHandler():void
{
httpService.url = searchURL + "Adobe";
httpService.send();
}
]]>
</fx:Script>
<fx:Declarations>
<s:HTTPService id="httpService"
result="ac = event.result.feed.entry as ArrayCollection" />
</fx:Declarations>
<s:List width="100%" height="100%"
dataProvider="{ac}"
itemRenderer="renderers.TweetRenderer"/>
</s:Application>
Styles
Now that we have more elements, our styles are growing a little bit.
We do not longer have the background-color property, because we are using images to paint the background instead. We have 2 images, one for the selected state and another for the rest (in this case we have only one, the up state). We use the pseudo selectors to target the different states but in both cases the name of the property is "background".
Other thing that we have now is individual style classes for the different text fields of the renderer. We have one for the name, one for the username and another for the content.
renderers|TweetRenderer
{
padding-top: 15;
padding-left: 10;
padding-right: 10;
padding-bottom: 15;
horizontal-gap: 10;
vertical-gap: 10;
name-style: nameRendererStyle;
user-style: userRendererStyle;
content-style: contentRendererStyle;
separator: Embed(source='/styles/images/separator.png' );
background: Embed(source='/styles/images/background_up.png',
scaleGridLeft=10, scaleGridTop=20, scaleGridRight=11, scaleGridBottom=21 );
}
renderers|TweetRenderer:selected
{
background: Embed(source='/styles/images/background_down.png',
scaleGridLeft=50, scaleGridTop=20, scaleGridRight=51, scaleGridBottom=21 );
}
.userRendererStyle
{
font-size: 20;
color: #222222;
font-family: _sans;
font-weight: bold;
}
.nameRendererStyle
{
font-size: 14;
color: #999999;
font-family: _sans;
}
.contentRendererStyle
{
font-size: 15;
color: #555555;
font-family: _sans;
}
Renderer
Because we want change the background when the user clicks in the item we need to implement the interface IItemRenderer, that allow the List to notify the renderers every time the selection changes. The help us with that task we create another class, the BaseRenderer that takes cares of the implementation of that interface and the TweetRenderer extends from it.
package renderers
{
import core.StyleClient;
import spark.components.IItemRenderer;
public class BaseRenderer extends StyleClient implements IItemRenderer
{
// Public Setters and Getters
protected var _data:Object;
public function set data( value:Object ):void
{
if( _data == value )
return;
_data = value;
// if the elements has been created we set the values
if( creationComplete )
setValues();
}
public function get data( ):Object
{
return _data;
}
// selected-------------------------------------------------------------
protected var _selected:Boolean = false;
public function get selected():Boolean
{
return _selected;
}
public function set selected(value:Boolean):void
{
if (value != _selected)
{
_selected = value;
updateSkin();
}
}
// dragging------------------------------------------------------------
/** Property not used but it is required by the interface IItemRenderer */
protected var _dragging:Boolean;
public function set dragging( value:Boolean ):void
{
_dragging = value;
}
public function get dragging():Boolean
{
return _dragging;
}
// showsCaret-------------------------------------------------------------
/** Property not used but it is required by the interface IItemRenderer */
protected var _showsCaret:Boolean;
public function set showsCaret( value:Boolean ):void
{
_showsCaret = value;
}
public function get showsCaret():Boolean
{
return _showsCaret;
}
// itemIndex--------------------------------------------------------------
protected var _itemIndex:int;
public function set itemIndex( value:int ):void
{
_itemIndex = value;
}
public function get itemIndex():int
{
return _itemIndex;
}
// itemIndex--------------------------------------------------------------
protected var _label:String;
public function get label():String
{
return _label;
}
public function set label(value:String):void
{
_label = value;
}
// --------------------------------------------------------------
protected function updateSkin():void
{
// To be implemented in children
}
protected function setValues():void
{
// To be implemented in children
}
}
}
I added 2 methods to this base class: setValues which is called every time that the data changes and updateSkin which is called every time that the selection changes.
In the method updateSkin that is implemented in the TweetRenderer we change the state of the skin. Changing the state gives us new values for our call to the getStyle method. For example, when we change to the "selected" state and we call getStyle( "background" ) we get the image that is defined inside the pseudo selector TweetRenderer:selected. When we are in the "up" state, we get the default value for the background property. All this CSS functionally is provided by extending StyleClient.
You can see that in the following code
package renderers
{
import flash.display.DisplayObject;
import flash.text.TextField;
import spark.primitives.BitmapImage;
import spark.primitives.Graphic;
import utils.TextUtil;
public class TweetRenderer extends BaseRenderer
{
// Protected properties
protected var userField:TextField;
protected var nameField:TextField;
protected var contentField:TextField;
protected var avatar:BitmapImage;
protected var avatarHolder:Graphic;
protected var background:DisplayObject;
protected var backgroundClass:Class;
protected var separator:DisplayObject;
protected var paddingLeft:int;
protected var paddingRight:int;
protected var paddingBottom:int;
protected var paddingTop:int;
protected var horizontalGap:int;
protected var verticalGap:int;
// Contructor
public function TweetRenderer()
{
percentWidth = 100;
}
// Override Protected Methods
override protected function createChildren():void
{
readStyles();
setBackground();
var separatorAsset:Class = getStyle( "separator" );
if( separatorAsset )
{
separator = new separatorAsset();
addChild( separator );
}
userField = TextUtil.createSimpleTextField( getStyle( "userStyle" ) );
addChild( userField );
nameField = TextUtil.createSimpleTextField( getStyle( "nameStyle" ) )
addChild( nameField );
contentField = TextUtil.createSimpleTextField( getStyle( "contentStyle" ) , false, "none" );
contentField.wordWrap = true;
contentField.multiline = true;
addChild( contentField );
avatarHolder = new Graphic();
avatar = new BitmapImage();
avatar.fillMode = "clip";
avatarHolder.width = 48;
avatarHolder.height = 48;
avatarHolder.addElement( avatar );
addChild( avatarHolder );
// if the data is not null, set the text
if( data )
setValues();
}
protected function setBackground():void
{
var backgroundAsset:Class = getStyle( "background" );
if( backgroundAsset && backgroundClass != backgroundAsset )
{
if( background && contains( background ) )
removeChild( background );
backgroundClass = backgroundAsset;
background = new backgroundAsset();
addChildAt( background, 0 );
if( layoutHeight && layoutWidth )
{
background.width = layoutWidth;
background.height = layoutHeight;
}
}
}
override protected function updateDisplayList( unscaledWidth:Number, unscaledHeight:Number ):void
{
avatarHolder.x = paddingLeft;
avatarHolder.y = paddingTop;
avatarHolder.setLayoutBoundsSize( avatarHolder.getPreferredBoundsWidth(), avatarHolder.getPreferredBoundsHeight() );
userField.x = avatarHolder.x + avatarHolder.width + horizontalGap;
userField.y = paddingTop;
nameField.x = userField.x + userField.textWidth + horizontalGap;
nameField.y = paddingTop + ( userField.textHeight - nameField.textHeight ) / 2;
contentField.x = avatarHolder.x + avatarHolder.width + horizontalGap;
contentField.y = paddingTop + userField.textHeight + verticalGap;
contentField.width = unscaledWidth - paddingLeft - paddingRight - avatarHolder.getLayoutBoundsWidth() - horizontalGap;
layoutHeight = Math.max( contentField.y + paddingBottom + contentField.textHeight, avatarHolder.height + paddingBottom + paddingTop );
background.width = unscaledWidth;
background.height = layoutHeight;
separator.width = unscaledWidth;
separator.y = layoutHeight - separator.height;
}
override public function getLayoutBoundsHeight(postLayoutTransform:Boolean=true):Number
{
return layoutHeight;
}
override protected function setValues():void
{
var arr:Array = String( data.author.name ).split("(");
var user:String = String( data.author.name )
userField.text = arr[0];
nameField.text = String( arr[ 1 ] ).replace( ")", "" );
contentField.htmlText = data.content.value;
if( data.link.length > 1)
avatar.source = data.link[ 1 ].href;
}
override protected function updateSkin():void
{
currentCSSState = ( selected ) ? "selected" : "up";
setBackground();
}
protected function readStyles():void
{
paddingTop = getStyle( "paddingTop" );
paddingLeft = getStyle( "paddingLeft" );
paddingRight = getStyle( "paddingRight" );
paddingBottom = getStyle( "paddingBottom" );
horizontalGap = getStyle( "horizontalGap" );
}
}
}
We also have now more children to create and layout. We have an image to load for the avatar, the embedded images for the background and a bunch of text fields. For the text fields, we use the same TextUtil to help us on the task of reading the styles and setting the properties. You may notice that we have 3 different styles, one for each text field. The TextUtil can read the styles from a class or from the type selector or current object ("this"). In this case we use 3 different classes.
The other thing that I implemented in this renderer is the ability to have variable height. To make it work with the list I just override the method getLayoutBoundsHeight that returns the height of the renderer after the layout is done. The List will use that value to layout all the items one after the other even if the height is different for each renderer.
Lee Burrows
Great set of articles on renderers - very clear and informative. Thanks a lot.
I've heard that getStyle is quite an expensive call? Is that something that should be limited when creating a mobile renderer?
Cheers,
Lee
Nahuel Foronda
john
thank you for this really great article about mobile Item Renderers! This helped me A LOT!
I have a problem thought.
I want to use embedded fonts for my textFields that I create using "TextUtil.createSimpleTextField(this)" but it seems I cant get them to render properly..
I am using the following attributes in my main.css:
@font-face
{
src: url("assets/fonts/Base6.ttf");
fontFamily: iOSFontCFF;
fontStyle: normal;
fontWeight: bold;
embedAsCFF: true;
advancedAntiAliasing: true;
}
@font-face
{
src: url("assets/fonts/Base6.ttf");
fontFamily: iOSFontNonCFF;
fontStyle: normal;
fontWeight: bold;
embedAsCFF: false;
advancedAntiAliasing: true;
}
renderers|ZCategoryRenderer
{
font-family: iOSFontNonCFF;
padding-left: 6;
padding-right: 10;
padding-top: 6;
padding-bottom: 8;
font-size: 14;
fontWeight:bold;
name-field-font-color : #000000;
number-field-font-color : #ffffff;
name-field-drop-shadow: #ffffff;
number-field-drop-shadow: #000000;
}
It seems my textFields in ZCategoryRenderer get all the styling except the "font-family"...
The same font WORKS for spark buttons and Labels in my application (iOSFontNonCFF/iOSFontCFF)
Any ideas?
Thanks.
john
Inside ZTextUtil.as in the method createSimpleTextField I just had to add "textField.embedFonts = true".
Thanks again!
msmdesign
When using AS instead of Flex component the list.dataGroup.measuredSize fires as 0.
When loading in different sized images to the avatar the images get stretched to fit avatarHolder.
If anyone has been able to work around can please post ideas, thanks.
Again, great post on using AS3/CSS for item renderes, thanks.
Nahuel Foronda
avatarHolder.setLayoutBoundsSize( width, height );
You can calculate the size of your images and set the size of the holder proportionally.
Regarding the list.dataGroup.measuredSize you can override the getPreferredBoundsHeight(postLayoutTransform:Boolean=true):Number function in your renderer to give the list a preferred value of the renderer, same with the width property.
msmdesign
vikas
By the way
verticalGap getStyle() is missing.
martin
But I have a strange issue with this - I have a list with a dataprovider, the source of which is an ordered array of value objects. At first glance, visually everything makes sense and looks great - but when I update the dataprovider with new data, two things happen.
1. The size of the list isn't immediately updated - the scroll bar displays as if it only had a very small amount of information - however when I throw the list it resizes to the correct value as it goes towards the bottom of the list - this also happens in your example.
2. As I mentioned, my data provider is an ordered list, depending on certain flags, items at the top have different backgrounds, so I can immediately see the order in which items are rendered - for some reason, occasionally the list renders out of order - I have tried everything I can think of to resolve this - from Sort/SortFields to newing up the dataprovider but nothing seems to work.
If i revert to the standard item renderer both of these issues disappear. Which leads me to the conclusion that something strange is happening with the variable height code and measurement routines?
Much appreciated.
martin
override protected function measure():void
{
var totalHeight:Number = Math.max( enterLabel.y + enterLabel.textHeight + paddingBottom, avatarHolder.height + paddingBottom + paddingTop );
var totalWidth:Number = layoutWidth;
measuredWidth = totalWidth;
measuredHeight = totalHeight;
}
Although you'd change the totalHeight part back to your own values - thanks to mercilesshacking out of http://corlan.org/2011/07/11/creating-flex-mobile-lists-part-ii-using-virtualization/
Nahuel Foronda
Make sure that when the data is set in your renderer you call setValues and update the values of your inner children, after that the list will call getPreferredBoundsHeight to get the size of each cell, you can override that function to give a specific value that will the depends on each of your child depending on the data that you set it on them.
Nahuel Foronda
martin
John Higgins
Nahuel Foronda
Yes it is possible, make sure that you remove percentWidth = 100; and provide a value in getPreferredBoundsWidth so the list can layout the children horizontally.
John Higgins
Sumit Arora
Can you tell me how can we set variable row height property in mobile item renderer. I am struggling with that.
Regards,
Sumit Arora
Deepthi
Thanks for your great source on Item Renderers. I have a problem in List record selection.
In the spark list, I display 5 records. I select the 2 record. It goes to the Detail view. I click on Back Navigator button. It comes back to my list. This time, I reload the data as I can modify certain details in the Detail view. The problem what I face is, it does not highlight the 2nd record which was previously selected. The background image is not appearing for the 2nd record. How to apply the background once again for the previously selected row after the List is reloaded?
Regards,
Deepthi
Deepthi
I have one more question. In the function 'readTextFormat' (textUtil.as), how to set the textFormat as multiline and vertical center alignment?
Deepthi
Could you have some time to look at my issues. You can refer Point 18 and Point 19 comments. I got really stuck with these small issues but in a great sample.
Regards,
Deepthi
Patri
public function hasCSSState():Boolean
{
if(_currentCSSState != null)
return true;
else
return false;
}
Thank you very much for this great example.
Dmitry
My only concern is when I set useVirtualLayout="true" the scroll bar no longer works correctly - it always moves to the bottom irrelevant of the position of the view port, then once you scroll it jerks on the bottom until you reach the last element in List. After that it starts working properly.
per
Danut