Creating Flex Mobile Lists Part II – Using Virtualization

This is the second part (and the final one for that matter) of my post about creating Flex mobile lists (you can read the first part here). In this post I will show you how I modified the custom layout manager that I created in the first post to add support for virtualization. Also I will discuss the theory behind Flex list virtualization and creating custom item renderers.

As a reminder, in the first part I talked about you how you can create a custom list and layout manager that can be used for displaying vertical and tile section lists:

In this post I will show you how you can create layout managers that support virtualization and custom item renderers. You can see the two custom item renderers below:

I won’t discuss how I created the custom list component to support sections. For this read the first part of this series.

Layout Manager with and without Virtualization Support

So what does virtualization mean in the context of Flex lists and why is virtualization important? Suppose you have 100 items in a list and at any given time you can see no more than 20 items (to see the rest of the items, you have to scroll the list up or down ). If the layout manager used by the list doesn’t support virtualization then all the 100 items are rendered. The next image illustrates the concept.

A list that uses a layout manager that supports virtual layout renders only a limited number of items (typically equal with the number of items that fit in the list’s view port). When the list is scrolled, the item renderers used by the items that were moved out of the view port are recycled to render the items that just entered the view port. The next image illustrates the concept:

As I explained in the first part, if you have relatively few items (tens) you’ll get excellent performance using a layout manager without support for virtualization. However, in the next screencast I will show you what happens when you have 1,000 items in your list and you don’t use virtualization (all 1,000 items are created at once).

In the next video, you can see the same list and 1,000 items but this time using virtualization. As you can see, the list is created almost instantly as opposed to taking 5 or 6 seconds as in the previous video.

Creating a Custom Layout Manager

Let’s see how I created the layout manager you’ve seen in the previous videos. When you want to create a layout manager you have to override/implement at least two methods: measure() and updateDisplayList().

Using Virtualization

Now, let’s discuss what it is happening in terms of methods that are executed when you have a layout manager that uses virtualization.

First, your list component is instantiated and when the list’s DataGroup.validateSize() method is called the layout manager’s measure() methods gets called. The validateSize() method is executed at least once (when the list is created) and this represents the second pass of the layout (the first one is DataGroup.commitProperties()).

Next, as the DataGroup.validateDisplayList() method is executed (every time the list display list is invalidated) it will call the layout manager updateDisplayList() method.

So what to do you do inside of these two methods (measure() and updateDisplayList())?

measure()

You use the measure() method to calculate the total height/width of the list’s DataGroup and set these values back to the DataGroup. Remember that all the items that can’t be fit in the view port are clipped. To see them you have to scroll through the list. Well,  in order to get the scrolling to work correctly (this means that the size of the thumb is proportional to the number of items and you can scroll up to the first item and down to the last one) you have to calculate the total size occupied by all the elements as if all of them would be rendered. The catch is to do so without instantiating any item renderers.

My custom layout manager has properties for section title height, regular item height, and width. So these values are set by the programmer. This means, that I used these properties in the measure() method. You also need the data items. Remember that my list uses two different ways to render the data depending on type (section title or not). You can access the list data provider like this:

var dataProvider:IList = (target as DataGroup).dataProvider;

So, in my layout I loop through all of these items and calculate the total height (for a layout manager like Vertical Layout with items that have the same size, you wouldn’t need to loop through all the items; for example, you can multiply the height of one item by the total number of items in order to calculate the total height).

If you want to use the current size of the view port you can read the target.width and target.height properties. Take care when doing this because depending on the life cycle state of your list these values can be zero sometimes.

Once you calculated the total height and width you can set them to the DataGroup by calling:

target.setContentSize(totalWidth, totalHeight);

Here is the complete code for the measure() method (please note that the method is executed only when useVirtualLayout is set to true; otherwise I exit at the top of the method body):

  1. override public function measure():void {      
  2.         if (!useVirtualLayout)
  3.                 return;
  4.         var layoutTarget:GroupBase = target;
  5.         if (!layoutTarget)
  6.                 return;
  7.         var dataGroupTarget:DataGroup = layoutTarget as DataGroup;
  8.         if (dataGroupTarget.width == 0 || dataGroupTarget.height == 0) {
  9.                 _containerWidth = _containerHeight = 1;
  10.                 return;
  11.         }
  12.                
  13.         var totalWidth:Number = 0;
  14.         var totalHeight:Number = 0;
  15.         var dataProvider:IList = dataGroupTarget.dataProvider;
  16.         if (!dataProvider || !dataProvider.length)
  17.                 return;
  18.         var count:int = dataProvider.length;
  19.         var rowWidth:Number = dataGroupTarget.width;
  20.         var sectionHeight:Number = _sectionHeight;
  21.         var tileHeight:Number = _tileHeight;
  22.         var tileWidth:Number = _columnWidth;
  23.        
  24.         totalWidth = rowWidth;
  25.        
  26.         var elementWidth:Number, elementHeight:Number;
  27.         var x:Number = 0;
  28.         yToIndex = new Vector.<int>();
  29.         indexToY = new Vector.<int>();
  30.         var d:Object = d = dataProvider.getItemAt(0);
  31.         if (_sectionLabel in d) {
  32.                 addToVectorY(0, 0, sectionHeight);
  33.                 totalHeight = sectionHeight + _verticalGap;
  34.         } else {
  35.                 addToVectorY(0, 0, tileHeight);
  36.                 totalHeight = tileHeight + _verticalGap;
  37.         }
  38.         //loop though all the elements elements
  39.         for (var i:int = 0; i < count; i++) {
  40.                 d = dataProvider.getItemAt(i);
  41.                 if (!d) {
  42.                         elementWidth = tileWidth;
  43.                         elementHeight = tileHeight;
  44.                 } else if (_sectionLabel in d) {
  45.                         elementWidth = rowWidth;
  46.                         elementHeight = sectionHeight;
  47.                 } else {
  48.                         elementWidth = tileWidth;
  49.                         elementHeight = tileHeight;
  50.                 }
  51.                 // Would this element fit on this line, or should we move it
  52.                 // to the next line?
  53.                 if (x + elementWidth > rowWidth) {
  54.                         x = 0;
  55.                         //add the index to vector
  56.                         addToVectorY(i, totalHeight + 1, elementHeight);
  57.                         totalHeight += elementHeight + _verticalGap;
  58.                 }
  59.                 addToVectorIndex(i, totalHeight elementHeight _verticalGap);
  60.                 // Update the current position, add the gap
  61.                 x += elementWidth + _horizontalGap;
  62.         }
  63.         layoutTarget.measuredWidth = totalWidth;
  64.         layoutTarget.measuredHeight = totalHeight;
  65.         layoutTarget.measuredMinWidth = totalWidth;
  66.         layoutTarget.measuredMinHeight = totalHeight;
  67.         layoutTarget.setContentSize(totalWidth, totalHeight);
  68. }

updateDisplayList()

When the layout manager uses virtualization, the updateDisplayList() is called everytime you scroll through the list. Actually, when you scroll, first the scrollPositionChanged() method is called and if inside this method the target.invalidateDisplayList() method is called, then the updateDisplayList() method will be called. I will talk about the scrollPositionChanged() method in the next section.

The updateDisplayList() is called with two arguments: the width and height of the DataGroup view port. Inside of this method you will position and resize the items you want to display. Because we are talking about a virtual layout, you will not resize and position each element of the list. Ideally you will work only on those that are in the current view.

As you can imagine, the difficulty is in determining what elements must be displayed. The way I solved this is pretty simple. In the measure() method, when I loop through all the items, I save the correspondence between an item and its y value and between a y value and a data provider index (where the y value moves from 0 to the total height of the DataGroup content area).

How do you position and resize an item? You can use the ILayoutElement interface (the layout manager target property has a method named getVirtualElementAt() that you can use to retrieve one item):

// get the current element, we’re going to work with the
// ILayoutElement interface
element = target.getVirtualElementAt(i);

//resize the element
element.setLayoutBoundsSize(elementWidth, elementHeight);
// Position the element
element.setLayoutBoundsPosition(x, y);

Here is the complete code for updateDisplayList() when useVirtualLayout is set to true:

  1. override public function updateDisplayList(containerWidth:Number, containerHeight:Number):void {
  2.         if (useVirtualLayout)
  3.                 updateVirtual(containerWidth, containerHeight);
  4.         else
  5.                 updateNonVirtual(containerWidth, containerHeight);
  6.        
  7. }
  8.  
  9. /**
  10.  * Lay down the current items in the view – this is used when useVirtualLayout is set to true
  11.  */
  12. private function updateVirtual(containerWidth:Number, containerHeight:Number):void {
  13.         var layoutTarget:GroupBase = target;
  14.         if (!(layoutTarget as DataGroup).dataProvider || (layoutTarget as DataGroup).dataProvider.length == 0)
  15.                 return;
  16.        
  17.         if (!_containerWidth)
  18.                 _containerWidth = containerWidth;
  19.         if (!_containerHeight)
  20.                 _containerHeight = containerHeight;
  21.         //a resize of the component occured
  22.         if (_containerWidth != containerWidth || _containerHeight != containerHeight) {
  23.                 _containerWidth = containerWidth;
  24.                 _containerHeight = containerHeight;
  25.                 addExtraItems = 0;
  26.                 measure();
  27.                 //set the new _firstIndex and _lastIndex
  28.                 scrollPositionChanged();               
  29.         }
  30.         trace(layoutTarget.numElements);
  31.         var x:Number = 0;
  32.         var y:Number = 0;
  33.         var maxWidth:Number = 0;
  34.         var maxHeight:Number = 0;
  35.         var elementWidth:Number, elementHeight:Number, prevElementHeight:Number;
  36.        
  37.         //provide the initial values
  38.         if (!_firstIndexInView)
  39.                 _firstIndexInView = 0;
  40.         if (!_lastIndexInView)
  41.                 _lastIndexInView = yToIndex.length 1 > layoutTarget.height ? yToIndex[layoutTarget.height + 1]  : layoutTarget.numElements 1;
  42.        
  43.         //add some extra rows after the current view
  44.         currentFirstIndex = _firstIndexInView;
  45.         if (currentFirstIndex < 0 )
  46.                 currentFirstIndex = 0;
  47.         if (!addExtraItems) {
  48.                 addExtraItems = Math.ceil(containerWidth / (_columnWidth + _horizontalGap)) * Math.ceil(containerHeight / ((_tileHeight + _sectionHeight) / 2));
  49.         }
  50.         currentLastIndex = _firstIndexInView + addExtraItems;
  51.         if (currentLastIndex > layoutTarget.numElements 1)
  52.                 currentLastIndex = layoutTarget.numElements 1;
  53.        
  54.         y = indexToY[currentFirstIndex];
  55.         var count:int = currentLastIndex + 1;
  56.         var element:ILayoutElement;
  57.        
  58.         for (var i:int = currentFirstIndex; i < count; i++) {
  59.                 // get the current element, we’re going to work with the
  60.                 // ILayoutElement interface
  61.                 element = layoutTarget.getVirtualElementAt(i);
  62.                 // Resize the element to its preferred size by passing
  63.                 // NaN for the width and height constraints
  64.                 element.setLayoutBoundsSize(NaN, NaN);
  65.                 if (element["data"] && _sectionLabel in element["data"]) {
  66.                         elementWidth = containerWidth;
  67.                         elementHeight = _sectionHeight;
  68.                 } else {
  69.                         elementWidth = _columnWidth;
  70.                         elementHeight = _tileHeight;
  71.                 }                              
  72.                 element.setLayoutBoundsSize(elementWidth, elementHeight);
  73.                
  74.                 // Would the element fit on this line, or should we move
  75.                 // to the next line?
  76.                 if (x + elementWidth > containerWidth) {
  77.                         x = 0;
  78.                         //move to the next row
  79.                         y += prevElementHeight + _verticalGap;
  80.                 }
  81.                 // Position the element
  82.                 element.setLayoutBoundsPosition(x, y);
  83.                 prevElementHeight = elementHeight;
  84.                 // Update the current position, add the gap
  85.                 x += elementWidth + _horizontalGap;
  86.         }
  87. }

scrollPositionChanged()

The final piece of the puzzle is the scrollPositionChanged() method. As I already said, this method is called every time the list is scrolled. You can find the current view port’s x and y properties by using the getScrollRect(). This method returns a Rectangle instance. Then you can read the top and bottom properties of the Rectangle instance to find where the DataGroup content is scrolled. For example:

var r:Rectangle = getScollRect();
var yTop:Number = r.top;
var yBottom:Number = r.bottom;

Then, using the values I saved when the measure() method was executed I can find the index of the the first item to be displayed and the index of the first item on the last visible row. I save these indexes and if they are outside of the indexes I rendered during the previous call to updateDisplayList() I invalidate the DataGroup display list, which in turn will call the layout manager updateDisplayList() method (and here I will use the two indexes I calculated in the scrollPositionChanged() method).

Please note that you can play around with these two indexes. For example you can add another row before the first visible row and one after the last visible row if you want to optimize small and unintended scroll actions (for example, you won’t execute the updateDisplayList() method if the list was scrolled 10 or 20 pixels).

Here is the complete code of scrollPositionChanged() (notice that it is executed only when useVirtualLayout is set to true; otherwise I just call the parent scrollPositionChanged()):

  1. override protected function scrollPositionChanged():void {
  2.         if (!useVirtualLayout) {
  3.                 super.scrollPositionChanged();
  4.                 return;
  5.         }
  6.                
  7.         var g:GroupBase = target;
  8.         if (!g)
  9.                 return;    
  10.         updateScrollRect(g.width, g.height);
  11.        
  12.         var n:int = g.numElements 1;
  13.         if (n < 0) {
  14.                 setIndexInView(1, 1);
  15.                 return;
  16.         }
  17.        
  18.         var scrollR:Rectangle = getScrollRect();
  19.         if (!scrollR) {
  20.                 setIndexInView(0, n);
  21.                 return;    
  22.         }
  23.        
  24.         var y0:Number = scrollR.top;
  25.         var y1:Number = scrollR.bottom .0001;
  26.         if (y1 <= y0) {
  27.                 setIndexInView(0, n);
  28.                 return;
  29.         }
  30.  
  31.         var i0:int, i1:int;
  32.         if (y0 < 0) {
  33.                 i0 = 0;
  34.                 i1 = yToIndex.length 1 > g.height ? yToIndex[g.height + 1]  : g.numElements 1;
  35.                 setIndexInView(i0, i1);
  36.                 return;
  37.         }
  38.                
  39.         if (y1 < yToIndex.length 1) {
  40.                 i0 = yToIndex[Math.floor(y0)];
  41.                 i1 = yToIndex[Math.ceil(y1)];
  42.         } else {
  43.                 if (yToIndex.length 1 g.height < 0)
  44.                         i0 = 0;
  45.                 else
  46.                         i0 = yToIndex[yToIndex.length 1 g.height];
  47.                 i1 = yToIndex[yToIndex.length 1];
  48.         }
  49.  
  50.         setIndexInView(i0, i1);
  51.         //invalidate display list only if we have items that are not already renderered
  52.         if (i0 < currentFirstIndex || i1 > currentLastIndex) {
  53.                 g.invalidateDisplayList();
  54.         }
  55. }

For the complete code of this layout manager please check the Download the code section of this post.

Without Virtualization

I enabled the same layout manager to work as one without virtualization support. As I said, there are situations when you prefer such a layout.

measure()

When the layout manager works without virtualization support, the measure() method does nothing (you have to implement it though if you extended BaseLayout; failing to do so will trigger a runtime error).

updateDisplayList()

This is where the heavy lifting will happen. This method will loop through all the items and position and resize each one. This method is executed only once as long as the DataGroup display list is not invalidated (you don’t change the list size or data provider for example).

Also, you have to set the DataGroup content size in order to enable scrolling on the list:

// Scrolling support – update the content size
target.setContentSize(width, height);

Here is the code:

  1. private function updateNonVirtual(containerWidth:Number, containerHeight:Number):void {
  2.         var layoutTarget:GroupBase = target;
  3.         if (!(layoutTarget as DataGroup).dataProvider || (layoutTarget as DataGroup).dataProvider.length == 0)
  4.                 return;
  5.        
  6.         if (!_containerWidth)
  7.                 _containerWidth = containerWidth;
  8.         if (!_containerHeight)
  9.                 _containerHeight = containerHeight;
  10.        
  11.         var x:Number = 0;
  12.         var y:Number = 0;
  13.         var maxWidth:Number = 0;
  14.         var maxHeight:Number = 0;
  15.         var elementWidth:Number, elementHeight:Number, prevElementHeight:Number;
  16.        
  17.         //add some extra rows after the current view
  18.         y = 0;
  19.         var count:int = layoutTarget.numElements;
  20.         var element:ILayoutElement;
  21.        
  22.         for (var i:int = 0; i < count; i++) {
  23.                 // get the current element, we’re going to work with the
  24.                 // ILayoutElement interface
  25.                 element = layoutTarget.getElementAt(i);
  26.                 // Resize the element to its preferred size by passing
  27.                 // NaN for the width and height constraints
  28.                 element.setLayoutBoundsSize(NaN, NaN);
  29.                 if (element["data"] && _sectionLabel in element["data"]) {
  30.                         elementWidth = containerWidth;
  31.                         elementHeight = _sectionHeight;
  32.                 } else {
  33.                         elementWidth = _columnWidth;
  34.                         elementHeight = _tileHeight;
  35.                 }                              
  36.                 element.setLayoutBoundsSize(elementWidth, elementHeight);
  37.                
  38.                 // Would the element fit on this line, or should we move
  39.                 // to the next line?
  40.                 if (x + elementWidth > containerWidth) {
  41.                         x = 0;
  42.                         //move to the next row
  43.                         y += prevElementHeight + _verticalGap;
  44.                 }
  45.                 // Position the element
  46.                 element.setLayoutBoundsPosition(x, y);
  47.                 prevElementHeight = elementHeight;
  48.                 // Update the current position, add the gap
  49.                 x += elementWidth + _horizontalGap;
  50.         }
  51.         // Scrolling support – update the content size
  52.         layoutTarget.setContentSize(containerWidth, y);
  53. }

scrollPositionChanged()

Again nothing to do here. You just call the super.scrollPositionChanged() method.

Creating Custom List Item Renderers

It is important to create item renderers that are extremely efficient. Otherwise all the work you put in creating and optimizing the custom layout manager will be for naught. For mobile development there are two built-in item renderers, both implemented using ActionScript. As a rule of thumb you don’t want to use MXML for graphics to create item renderers. Instead use FXG, bitmaps, and ActionScript.

The item renderers I created are designed to work with lists with or without sections. As I explained in the first part of this series, depending on the type of a data item (it either has a section property or it doesn’t) it is rendered differently.

Here are the methods you will most likely touch when developing custom item renderers:

  1. set data(). I override this method so I can check if the data to be set is a section title or not. Depending on this, I change the font styles and the value of the label property.
  2. createChildren(). This method is called automatically when an item renderer is instantiated. And this is where you create the parts you need. In the case of one of the item renderers I created, I only had to create an Image and a Sprite (the label part is created in the class I extend, LabelItemRenderer).
  3. updateDisplayList(). This method is called automatically when the item renderer is added to the DataGroup display list or the invalidateDisplayList() method is called. Inside of this method you can size and position the various parts. This method executes the following methods (in this order): drawBackground() and layoutContents(). If you draw on the background of the component using its graphics object then you have to call graphics.clear() before redrawing anything.
  4. drawBackground(). This method renders the background for the item renderer. For example this is where the different color for marking an item selection is drawn.
  5. layoutContent(). This method is used to position the children. You can use setElementPosition(child, x, y) or setElementSize(child, width, height).

For the project I worked on, I needed two different item renderers for the same list. Both item renderers must support two different appearance: one for a section title and one for a regular item. The first item renderer I created extends the built-in IconItemRenderer. I had to extend it in order to support the section titles. Here is the code:

  1. /**
  2.  *  Section Title font size
  3.  */
  4. [Style(name="sectionFontSize", type="Number", format="Length", inherit="no")]
  5.  
  6. public class ListItemRenderer extends IconItemRenderer {
  7.        
  8.         private var _backgroundSection:Number = 0xDDDDDD;
  9.        
  10.         public function set sectionFontSize(value:int):void {
  11.                 setStyle("sectionFontSize", value);
  12.         }
  13.        
  14.         public function set backgroundSection(value:Number):void {
  15.                 _backgroundSection = value;
  16.         }
  17.        
  18.         private var _sectionLabel:String = "section";
  19.        
  20.         public function set sectionLabel(value:String):void {
  21.                 if (value == _sectionLabel)
  22.                         return;
  23.                
  24.                 _sectionLabel = value;
  25.                 invalidateProperties();
  26.         }
  27.        
  28.         private var _normalLabelField:String = "label";
  29.        
  30.         public function set normalLabelField(value:String):void {
  31.                 _normalLabelField = value;
  32.         }
  33.        
  34.         /**
  35.          * Change the style based on the data: section item or regular item
  36.          */
  37.         override public function set data(value:Object):void {
  38.                 if (value) {
  39.                         if (value[_sectionLabel]) {
  40.                                 label = value[_sectionLabel];
  41.                                 labelDisplay.setStyle("textAlign", "center");
  42.                                 labelDisplay.setStyle("fontWeight", "bold");
  43.                                 labelDisplay.setStyle("fontSize", getStyle("sectionFontSize"));
  44.                                 iconWidth = 0;
  45.                         } else {
  46.                                 iconWidth = iconHeight;
  47.                                 label = value[_normalLabelField];
  48.                                 labelDisplay.setStyle("fontSize", getStyle("fontSize"));
  49.                                 labelDisplay.setStyle("textAlign", "left");
  50.                                 labelDisplay.setStyle("fontWeight", "normal");
  51.                         }
  52.                 }
  53.                 super.data = value;    
  54.         }
  55.        
  56.         /**
  57.          * Change the background color for section items
  58.          */
  59.         override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void {
  60.                 super.drawBackground(unscaledWidth, unscaledHeight);
  61.                
  62.                 if (data[_sectionLabel]) {
  63.                         graphics.beginFill(_backgroundSection, 1);
  64.                         graphics.lineStyle();
  65.                         graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
  66.                         graphics.endFill();
  67.                 }
  68.         }
  69. }

The second item renderer is just a simple “tile” for regular items. It displays an icon on top of the item and at the bottom, a label. I chose to extend the built-in LabelItemRenderer and add an Image and draw couple of things on top of the existing ones. Here is the source code:

  1. /**
  2.  *  Section Title font size
  3.  */
  4. [Style(name="sectionFontSize", type="Number", format="Length", inherit="no")]
  5.  
  6. public class TileItemRenderer extends LabelItemRenderer {
  7.        
  8.         private static var _imageCache:ContentCache;
  9.        
  10.         public function TileItemRenderer() {
  11.                 super();
  12.                 if (_imageCache == null) {
  13.                         _imageCache = new ContentCache();
  14.                         _imageCache.enableCaching = true;
  15.                         _imageCache.maxCacheEntries = 100;
  16.                 }
  17.         }
  18.        
  19.         private var _backgroundSection:Number = 0xDDDDDD;
  20.        
  21.         public function set backgroundSection(value:Number):void {
  22.                 _backgroundSection = value;
  23.         }
  24.        
  25.         public function set sectionFontSize(value:int):void {
  26.                 setStyle("sectionFontSize", value);
  27.         }
  28.  
  29.         public function set fontSize(value:int):void {
  30.                 setStyle("fontSize", value);
  31.         }
  32.        
  33.         private var _backgroundRegular:Number = 0xF4DD06;
  34.         private var _backgroundLabel:Number = 0xEAEAE8;
  35.        
  36.         private var _normalLabelField:String = "label";
  37.        
  38.         public function set normalLabelField(value:String):void {
  39.                 _normalLabelField = value;
  40.         }
  41.        
  42.         private var _sectionLabel:String = "section";
  43.        
  44.         public function set sectionLabel(value:String):void {
  45.                 if (value == _sectionLabel)
  46.                         return;
  47.                
  48.                 _sectionLabel = value;
  49.                 invalidateProperties();
  50.         }
  51.  
  52.         private var _iconField:String;
  53.        
  54.         /**
  55.          *  The name of the field in the data item to display as the icon.
  56.          *  By default <code>iconField</code> is <code>null</code>, and the item renderer
  57.          *  does not display an icon.
  58.          *
  59.          *  @default null
  60.          *
  61.          *  @langversion 3.0
  62.          *  @playerversion AIR 2.5
  63.          *  @productversion Flex 4.5
  64.          */
  65.         public function get iconField():String {
  66.                 return _iconField;
  67.         }
  68.        
  69.         /**
  70.          *  @private
  71.          */
  72.         public function set iconField(value:String):void {
  73.                 if (value == _iconField)
  74.                         return;
  75.                
  76.                 _iconField = value;
  77.                 invalidateProperties();
  78.         }
  79.        
  80.         /**
  81.          * Change the style based on the data: section item or regular item
  82.          */
  83.         override public function set data(value:Object):void {
  84.                 if (value) {
  85.                         if (value[_sectionLabel]) {
  86.                                 if (image)
  87.                                         image.visible = false;
  88.                                 if (labelBg)
  89.                                         labelBg.visible = false;
  90.                                 label = value[_sectionLabel];
  91.                                 labelDisplay.setStyle("textAlign", "center");
  92.                                 labelDisplay.setStyle("fontWeight", "bold");
  93.                                 labelDisplay.setStyle("fontSize", getStyle("sectionFontSize"));
  94.                         } else {
  95.                                 if (_iconField && value[_iconField]) {
  96.                                         if (image)
  97.                                                 image.visible = true;
  98.                                         image.source = value[_iconField];
  99.                                 } else {
  100.                                         if (image) {
  101.                                                 image.visible = false;
  102.                                                 image.source = "";
  103.                                         }
  104.                                 }
  105.                                 if (labelBg)
  106.                                         labelBg.visible = true;
  107.                                 label = value[_normalLabelField];
  108.                                 labelDisplay.setStyle("fontSize", getStyle("fontSize"));
  109.                                 labelDisplay.setStyle("textAlign", "left");
  110.                                 labelDisplay.setStyle("fontWeight", "normal");
  111.                         }
  112.                 }
  113.                 super.data = value;    
  114.         }
  115.        
  116.         //destroyIconDisplay() todo;
  117.         override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
  118.                 // clear the graphics before calling super.updateDisplayList()
  119.                 graphics.clear();
  120.                 super.updateDisplayList(unscaledWidth, unscaledHeight);
  121.                 //the following methods are called in super.updateDisplayList();
  122.                 //drawBackground(unscaledWidth, unscaledHeight);
  123.                 //layoutContents(unscaledWidth, unscaledHeight);
  124.         }
  125.        
  126.         private var image:Image;
  127.         private var labelBg:Sprite;
  128.         private var drawn:Boolean;
  129.  
  130.         override protected function createChildren():void {
  131.                 if (!image) {
  132.                         image = new Image();
  133.                         image.smooth = true;
  134.                         image.scaleMode = BitmapScaleMode.STRETCH;
  135.                         image.fillMode = BitmapFillMode.SCALE;
  136.                         image.contentLoader = _imageCache;
  137.                         addChild(image);
  138.                 }
  139.                 //create the background for label
  140.                 if (!labelBg) {
  141.                         labelBg = new Sprite();
  142.                         addChild(labelBg);
  143.                 }
  144.                 super.createChildren();
  145.         }
  146.        
  147.         /**
  148.          * Change the background color for section items
  149.          */
  150.         override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void {
  151.                 super.drawBackground(unscaledWidth, unscaledHeight);
  152.                
  153.                 if (data[_sectionLabel]) {
  154.                         graphics.beginFill(_backgroundSection, 1);
  155.                         graphics.lineStyle();
  156.                         graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
  157.                         graphics.endFill();
  158.                 } else {
  159.                         //add a vertical line to the right of the item                 
  160.                         var rightSeparatorColor:uint = 0x000000;
  161.                         var rightSeparatorAlpha:Number = .3;
  162.                         graphics.beginFill(rightSeparatorColor, rightSeparatorAlpha);
  163.                         graphics.drawRect(unscaledWidth 1, 0, 1, unscaledHeight);
  164.                         graphics.endFill();
  165.                         //draw a rounded corner place holder for text and icon
  166.                         //let a padding around for seeing through the selection
  167.                         graphics.beginFill(_backgroundRegular, 1);
  168.                         graphics.lineStyle();
  169.                         graphics.drawRoundRect(3, 3, unscaledWidth 6, unscaledHeight 6, 10, 10);
  170.                         graphics.endFill();
  171.                 }
  172.         }
  173.        
  174.         override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void {
  175.                 super.layoutContents(unscaledWidth, unscaledHeight);
  176.                
  177.                 //position the image
  178.                 if (!data[_sectionLabel] && _iconField && data[_iconField]) {
  179.                         setElementPosition(image, 5, 5);
  180.                         setElementSize(image, unscaledWidth 10, unscaledWidth 10);
  181.                 }
  182.                
  183.                 if (!data[_sectionLabel] && labelDisplay) {
  184.                         labelDisplay.commitStyles();
  185.                         var h:Number = labelDisplay.height;
  186.                        
  187.                         //draw for holding the text
  188.                         if (!drawn) {
  189.                                 drawn = true;
  190.                                 labelBg.graphics.clear();
  191.                                 labelBg.graphics.beginFill(_backgroundLabel, 1);
  192.                                 labelBg.graphics.lineStyle();
  193.                                 labelBg.graphics.drawRoundRect(3, unscaledHeight 10 h, unscaledWidth 6, h + 6, 10, 10);
  194.                                 labelBg.graphics.endFill();
  195.                                 //                                      setElementPosition(labelBg, 0, unscaledHeight – 10 – h);
  196.                         }
  197.                         var paddingLeft:Number = getStyle("paddingLeft");
  198.                         //reposition the label at the bottom of the item
  199.                         setElementPosition(labelDisplay, paddingLeft, unscaledHeight h);
  200.                 }
  201.         }
  202.        
  203. }

You can watch this screencast to see these item renderers in action together with the custom layout manager and list:

And here is the same code running on my Nexus One:

Conclusions

I hope that I managed to shed some light on the dark art of creating custom lists/layout managers/item renderers. If you need to create something like this, I encourage you to take a look at how the built-in layout managers and item renderers were implemented. You will learn a lot. Also, especially when creating mobile applications try to use/extend the built-in Flex classes. The Flex SDK team put a lot of effort into optimizing this code. So you will get all the future improvements for “free”.

If you have created custom lists maybe you can drop a comment and share with us your work.

Download the Code

You can download all the code used in this series from my GitHub account.

8 thoughts on “Creating Flex Mobile Lists Part II – Using Virtualization

  1. Pingback: Creating Flex Mobile Section Lists : Mihai Corlan

  2. Great article! Thanks for taking the time to write out this information.

    Clearly using static images, such as bitmaps and fxgs are the most efficient implementation of graphics, but I’ve found that one can use DisplayObjects effectively. In fact, I’ve used MXML graphics in a list with awesome performance.

    Sometimes the business case calls for dynamically created graphical content in a list. For those needs, I implemented a DisplayObject list. Here’s a link.

    http://dreamingwell.com/articles/archives/2011/06/flex-mobile-dis-1.php

  3. Pingback: AIR Mobile – Créer une liste avec groupement (type liste de contacts iOS / Android) « Adobe Flex Tutorial

  4. How can we load image from ‘app:/’ domain to skin which extend IconItemRenderer? I’m trying to do this but without success :(((

    thanks for any help.

    cheers

  5. @Paw

    look at the source code of my sample. The same way I’m loading the photos in my list…

    cheers,
    Mihai Corlan

  6. Hi Mihai,

    I’ve added extra text field to the normal list item, but for some reasons, the text field repeat at the header section. Can you guide me on how to add extra text field to the list item?

Leave a Reply

Your email address will not be published. Required fields are marked *