Creating Flex Mobile Section Lists

For a mobile application I’m working on I need a section list. If you are familiar with the Flex framework you know that you don’t have built-in section list components (you can have vertical, horizontal, and tile lists). I played a little bit with the Flex List, IconItemRenderer, and BasicLayout classes and managed to put together two section lists: a vertical section list and a tile section list. In this post I will talk about how I did it and about some theory behind the list components.

What is a Section List?

Before talking about code, let’s establish what a section list is:

  • It is a list that provides a way to render items grouped in sections. For example, if you have a data structure like “Fruits, Banana, Cherry, Kiwi, Cheese, Cheddar, Gorgonzola” a section list will render this data in a way that makes it clear that Fruits/Banana/Cherry/Kiwi is one section and Cheese/Cheddar/Gorgonzola is another one;
  • The section title items can’t be selectable. In our example, clicking on Fruits or Cheese items will not throw a select event nor will it select the item;
  • Section title items should be rendered so that it is clear they are different from regular items.

Here is an example of a vertical section list (blue items are section title items, the gray Blueberries item represents the current selection):

If you wonder why you’d need such a list, here are a couple of examples where you can find it pretty useful:

  • You have a list of people and you want to display the items sorting alphabetically. By creating sections A, B, C, and so on users can easily navigate through the sections to find what they are looking for;
  • You have a properties or preference page in your application with a bunch of options (for example, Editing, Account, and so forth). You can nicely organize the information so users can find their way quickly.

Some Theory Behind Flex lists

This is the boring section of this post :) If you don’t like it, feel free to skip it :D

Flex mobile lists inherit the architecture of the Spark components introduced in Flex 4 back in 2010. An oversimplified dissection of a Flex mobile list would reveal these “organs”: list component, DataGroup, Scroller, layout manager, and item renderer. By default a Flex mobile list is a vertical list (items are rendered vertically and if there are more than can be fitted on the view port than you can scroll the list up and down).

Here is a description of how these things work together:

  • The list component uses a container for holding the items that will be displayed. This container is an instance of the DataGroup class.
  • The DataGroup container implements the IViewport interface. If the items to be displayed don’t fit in the viewport, they are clipped.
  • To scroll the viewport of the DataGroup and see the rest of the items, a Scroller is used. In the case of the mobile Flex list the Scroller displays a scrollbar only when you interact with the list (by default it is hidden). Its size gives a visual clue about the number of the elements from the list (the more elements in the list, the smaller the thumb).
  • The list component uses a layout manager. This class is in control of how and where the items displayed in the DataGroup are rendered.
  • Each item of the list is displayed using an item renderer. The Flex framework gives you two built-in item renderers for lists: IconItemRenderer and LabelRenderer. The item renderer class is in charge of how an item will be rendered on the screen. Lists are designed to work with a single item renderer.
  • The list component has a property called dataProvider that you can use to set the data you want to display. Typically you’d assign to this property and Array or an ArrayCollection of Strings or Objects.
  • Finally, mobile lists use virtualization (the virtualization part is implemented actually by the layout manager). This means that only the items that are visible in the viewport are rendered. And when the viewport is moved (for example, you scroll the list down) the item renderers of the items that were moved out of the view port are reused for the items that just got in the view port. This way, even on a mobile device, you can have lists with 10,000 items and you want run out of memory. However there is still a cost with this approach: the DataGroup has to execute a bunch of removeChildAt() and addChildAt() calls in order to reuse the item renderer instances.

I won’t get into details about how everything is linked together at runtime and what methods are called and when. I’ll save this for another post.

The first section list I wanted to create was a vertical section list. Let’s see how I implement it.

Creating a Vertical Section List

I looked at the built-in Flex mobile list and I realized that making a Vertical Section List would be quite easy. The VerticalLayout manager does all the work I needed, and the only things I had to change were:

  1. Disabling item selection for list items that are section titles;
  2. Finding a simple way to define the data provider structure (which data items are section title items and which are just regular items).
  3. Extend one of the two built-in item renderers so I can have two distinct looks: one for section title items and one for regular items;

First, I decided on how to model the data provider used for the SectionList. I decided that I want to be able to use an Array or ArrayCollection of objects and I will use different properties to store the label for section title items and regular items.

Next, I created a new class that extends the Flex List class. In this class I override the item_mouseDownHandler() method. This method is executed each time you touch the list. So all I had to do is to check whether the item touched is a section title item or not. If it is, then I just call event.preventDefault() to cancel the selection.

Next, I added a property named sectionField to store the property name used for defining a data as an section list. Here is the code of the SectionList class:

  1. package org.corlan.components {
  2.        
  3.         import flash.events.MouseEvent;
  4.         import spark.components.IItemRenderer;
  5.         import spark.components.List;
  6.        
  7.         public class SectionList extends List {
  8.                
  9.                 /**
  10.                  * The property name for data that are section title items
  11.                  */
  12.                 private var _sectionField:String;
  13.                
  14.                 public function set sectionField(value:String):void {
  15.                         _sectionField = value;
  16.                 }
  17.  
  18.                 /**
  19.                  * Disable selection for section title items
  20.                  */
  21.                 override protected function item_mouseDownHandler(event:MouseEvent):void {
  22.                         var data:Object;
  23.                         if (event.currentTarget is IItemRenderer)
  24.                                 data = IItemRenderer(event.currentTarget).data;
  25.                         if (data && data[_sectionField])
  26.                                 event.preventDefault();
  27.                         super.item_mouseDownHandler(event);
  28.                 }
  29.                
  30.         }
  31. }

Then I created a new item renderer by extending the built-in mobile item renderer IconItemRenderer. IconItemRenderer enables you to display more than just a label (you can have a decorator, an image, and a subtitle). To tell you the truth, I have used only the label. So, I guess I could have chosen the LabelRenderer as well.

The first thing I did was to override the set data() method. This method is called automatically everytime the item renderer class is used to render a data item. So this is the perfect place to check if the data that is about to be displayed is a section title item or not. Depending on the result I change the font alignment and weight and I change the labelField value to the property name to be used as the source of the text.

Next, I override the drawBackground() method. This method draws the background of the item. Depending on what data I have to render I change the color of the background.

Finally, I added three new properties that allow me to set the name of the property that holds a section title item, a regular item, and background color to be used for section title items: sectionField and normalLabelField.

Here is the code of the custom item renderer:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <s:IconItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
  3.                                         xmlns:s="library://ns.adobe.com/flex/spark"  
  4.                                         width="100%" iconWidth="64" iconHeight="64">
  5.         <fx:Script>
  6.                 <![CDATA[
  7.                        
  8.                         private var _backgroundSection:Number = 0xDDDDDD;
  9.                        
  10.                         public function set backgroundSection(value:Number):void {
  11.                                 _backgroundSection = value;
  12.                         }
  13.                        
  14.                         private var _normalLabelField:String = "label";
  15.  
  16.                         public function get normalLabelField():String {
  17.                                 return _normalLabelField;
  18.                         }
  19.                        
  20.                         public function set normalLabelField(value:String):void {
  21.                                 _normalLabelField = value;
  22.                         }
  23.                        
  24.                         private var _sectionField:String = "section";
  25.  
  26.                         public function get sectionField():String {
  27.                                 return _sectionField;
  28.                         }
  29.                        
  30.                         public function set sectionField(value:String):void {
  31.                                 if (value == _sectionField)
  32.                                         return;
  33.                                
  34.                                 _sectionField = value;
  35.                                 invalidateProperties();
  36.                         }
  37.                        
  38.                         /**
  39.                          * Change the style based on the data: section item or regular item
  40.                          */
  41.                         override public function set data(value:Object):void {
  42.                                 if (value[_sectionField]) {
  43.                                         labelField = _sectionField;
  44.                                         labelDisplay.setStyle("textAlign", "center");
  45.                                         labelDisplay.setStyle("fontWeight", "bold");
  46.                                 } else {
  47.                                         labelField = _normalLabelField;
  48.                                         labelDisplay.setStyle("textAlign", "left");
  49.                                         labelDisplay.setStyle("fontWeight", "normal");
  50.                                 }
  51.                                 super.data = value;    
  52.                         }
  53.                        
  54.                         override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void {
  55.                                 super.drawBackground(unscaledWidth, unscaledHeight);
  56.                                 //change the background if we render for a section title item
  57.                                 if (data[_sectionField]) {
  58.                                         graphics.beginFill(_backgroundSection, 1);
  59.                                         graphics.lineStyle();
  60.                                         graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
  61.                                         graphics.endFill();
  62.                                 }
  63.                         }
  64.                        
  65.                 ]]>
  66.         </fx:Script>
  67. </s:IconItemRenderer>

With all this sorted out, here is how you use the SectionList component in your code:

  1. <components:SectionList id="list"
  2.         x="0" y="0" width="100%" height="100%"
  3.         sectionField="section" dataProvider="{arr}">
  4.  
  5.         <components:itemRenderer>
  6.                 <fx:Component>
  7.                         <skins:SectionListItemRenderer
  8.                                 normalLabelField="label"
  9.                                 sectionField="section"
  10.                                 backgroundSection="0x1d9cd1"/>
  11.                 </fx:Component>
  12.         </components:itemRenderer>
  13.  
  14. </components:SectionList>

This example uses the data provider defined below. This is why I set the sectionField property of the SectionList and SectionListItemRenderer to “section“. You can see how the background color for section title items is set.

Defining the data provider:

  1. <s:ArrayCollection id="arr">
  2.         <fx:Object section="Fruits"/>
  3.                 <fx:Object label="Banana"/>
  4.                 <fx:Object label="Blueberries"/>
  5.                 <fx:Object label="Cherry"/>
  6.                 <fx:Object label="Kiwi"/>
  7.         <fx:Object section="Vegetables"/>
  8.                 <fx:Object label="Bean"/>
  9.                 <fx:Object label="Cabbage"/>
  10.                 <fx:Object label="Cucumber"/>
  11. </s:ArrayCollection>

Here’s a video with the list running in the Flash Builder desktop simulator and on my Android phone:

Creating a Tile Section List

A tile section list could be handy, especially on tablets. Because you have plenty of space on the horizontal axis you can fit multiple items on the same row and thus make better use of the available screen on the device. Here is an example of tile section list (the item renderer used here doesn’t do a good job of separating visually adjacent items):

If all you need is a tile list then you can use the Flex list with the built-in TileLayout manager.

With the vertical section list up and running all I had to do was to create a new layout manager. This layout manager lays out the items (starting from left to right and top to bottom) using these rules:

  • If the item is a regular item and it is enough room to accommodate it on the current row it will draw the item next to the previous one;
  • If the item is a regular item and there is not enough room to fit the item on the current row, it goes on the next row;
  • If the item is a section title item it goes on the next row and fills is the entire view port width.

Fortunately, back in 2010, Evtim Georgiev (an engineer on the Flex SDK team) created a custom layout that was pretty close to what I needed. So I took this layout manager and modified it a little bit. The magic happens in the updateDisplayList() method. This method is called every time the display list of the List is invalidated (for example if you change the width/height of the list or when the list component is initialized). The two arguments of this method represent the view port’s height and width.

updateDisplayList() method is where you position and size the items. For my first run I chose to create a layout manager that doesn’t use virtualization. This means that updateDisplayList() will iterate over all the items you have in the list and create an item renderer for each one and then position and size the item. The layout manager positions the items in the DataGroup instance used by the list for displaying the items. The item renderer class implements the ILayoutElement interface. Using this API, the layout manager can set the width/height and x/y of each item renderer.

As I explained, the layout manager uses the whole width of the view port for section title items. For regular items, I added a property to the layout manager called columnWidth – so you can control how many items you can have on the same row.

Here is the code of this custom layout manager (I called it SectionListLayout):

  1. //see Evtim Georgiev article http://www.adobe.com/devnet/flex/articles/spark_layouts.html
  2. package org.corlan.layout {
  3.        
  4.         import mx.core.ILayoutElement;
  5.         import spark.components.supportClasses.GroupBase;
  6.         import spark.layouts.BasicLayout;
  7.        
  8.         public class SectionListLayout extends BasicLayout {
  9.                
  10.                 private var _horizontalGap:Number = 0;
  11.                 private var _verticalGap:Number = 0;
  12.                 private var explicitColumnWidth:Number;
  13.                 private var _columnWidth:Number = 250;
  14.                
  15.                 public function SectionListLayout() {
  16.                         super();
  17.                 }
  18.                
  19.                 public function set horizontalGap(value:Number):void {
  20.                         _horizontalGap = value;
  21.                        
  22.                         // We must invalidate the layout
  23.                         var layoutTarget:GroupBase = target;
  24.                         if (layoutTarget) {
  25.                                 layoutTarget.invalidateSize();
  26.                                 layoutTarget.invalidateDisplayList();
  27.                         }
  28.                 }
  29.                
  30.                 public function set verticalGap(value:Number):void {
  31.                         _verticalGap = value;
  32.                        
  33.                         // We must invalidate the layout
  34.                         var layoutTarget:GroupBase = target;
  35.                         if (layoutTarget) {
  36.                                 layoutTarget.invalidateSize();
  37.                                 layoutTarget.invalidateDisplayList();
  38.                         }
  39.                 }
  40.                
  41.                 public function set columnWidth(value:Number):void {
  42.                         explicitColumnWidth = value;
  43.                         if (value == _columnWidth)
  44.                                 return;
  45.                        
  46.                         _columnWidth = value;
  47.                         var layoutTarget:GroupBase = target;
  48.                         if (layoutTarget) {
  49.                                 layoutTarget.invalidateSize();
  50.                                 layoutTarget.invalidateDisplayList();
  51.                         }
  52.                 }
  53.                
  54.                 // When extending the BasicLayout
  55.                 // you have to override this method
  56.                 override public function measure():void {
  57.  
  58.                 }
  59.                
  60.                 //
  61.                 override public function updateDisplayList(containerWidth:Number, containerHeight:Number):void {
  62.                         // The position for the first element
  63.                         var x:Number = 0;
  64.                         var y:Number = 0;
  65.                         var maxWidth:Number = 0;
  66.                         var maxHeight:Number = 0;
  67.                         var elementWidth:Number, elementHeight:Number;
  68.  
  69.                         // loop through the elements
  70.                         var layoutTarget:GroupBase = target;
  71.                         var count:int = layoutTarget.numElements;
  72.                         var element:ILayoutElement;
  73.                         for (var i:int = 0; i < count; i++) {
  74.                                 // get the current element, we’re going to work with the
  75.                                 // ILayoutElement interface
  76.                                 element = useVirtualLayout ?
  77.                                         layoutTarget.getVirtualElementAt(i) :
  78.                                         layoutTarget.getElementAt(i);
  79.                                
  80.                                 // Resize the element to its preferred size by passing
  81.                                 // NaN for the width and height constraints
  82.                                 element.setLayoutBoundsSize(NaN, NaN);
  83.                                 elementHeight = element.getLayoutBoundsHeight();
  84.                                 if ((element as Object).data
  85.                                                 && (element as Object).data.section) {
  86.                                        
  87.                                         element.setLayoutBoundsSize(containerWidth, elementHeight);
  88.                                         elementWidth = containerWidth;
  89.                                 } else {
  90.                                         element.setLayoutBoundsSize(_columnWidth, elementHeight);
  91.                                         elementWidth = _columnWidth;
  92.                                 }                              
  93.  
  94.                                 // Would the element fit on this line, or should we move
  95.                                 // to the next line?
  96.                                 if (x + elementWidth > containerWidth) {
  97.                                         x = 0;
  98.                                         y += elementHeight + _verticalGap;
  99.                                 }
  100.                                                                
  101.                                 // Position the element
  102.                                 element.setLayoutBoundsPosition(x, y);
  103.                                
  104.                                 // Find maximum element extents. This is needed for
  105.                                 // the scrolling support.
  106.                                 maxWidth = Math.max(maxWidth, x + elementWidth);
  107.                                 maxHeight = Math.max(maxHeight, y + elementHeight);
  108.                                
  109.                                 // Update the current position, add the gap
  110.                                 x += elementWidth + _horizontalGap;
  111.                         }
  112.                        
  113.                         // Scrolling support – update the content size
  114.                         layoutTarget.setContentSize(maxWidth, maxHeight);
  115.                 }
  116.         }
  117. }

The item renderer and list data provider are the same as the ones used for the vertical section list. I’m using the same SectionList I created earlier. Here is how I instantiate the SectionList and set the various properties:

  1. <components:SectionList id="list" x="0" y="0"
  2.         width="100%" height="100%"
  3.         sectionField="section"
  4.         dataProvider="{arr}">
  5.        
  6.     <components:layout>
  7.         <layout:SectionListLayout columnWidth="200"/>
  8.     </components:layout>
  9.        
  10.     <components:itemRenderer>
  11.         <fx:Component>
  12.             <skins:SectionListItemRenderer
  13.                 normalLabelField="label"
  14.                 sectionField="section"
  15.                 backgroundSection="0x1d9cd1"/>
  16.         </fx:Component>
  17.     </components:itemRenderer>
  18. </components:SectionList>

And here is a video with the tile section list in action:

Caveat

Obviously, a layout manager that doesn’t support virtualization could trigger performance issues when you have hundreds or thousands of items (at least on mobile devices; on the desktop you probably won’t run into this most of the time). However, if you don’t have many items, than it could be actually faster than a layout that uses virtualization.

LATER UPDATE: You can read here how you can create layout managers that support virtualization.

Why? Because when a list uses virtualization, it recycles the item renderers (as soon as you move out of the screen an item, its item renderer is reused for an item that just got into the view port). This means that as each item renderer is recycled, it must be removed from the DataGroup display list, new data are set, the item is redrawn if necessary, and it is added to the DataGroup display list.

Right now I’m working on a Tile Section Layout manager with support for virtualization. Once I finish it, I will write another blog post that will explain in greater depth “the art” of creating layout managers.

Download the code

You can download the source code of the Flex mobile project from my GitHub account.

8 thoughts on “Creating Flex Mobile Section Lists

  1. Pingback: Creating Flex Mobile Lists Part II – Using Virtualization : Mihai Corlan

  2. Now I know what you call these things. :-)

    I had this problem myself when building the RunPee AIR app – Android/iTunes marketplaces. I have a list of movies and I wanted to be able to group them by date or first-letter. I solved the problem pretty much the same way you did. Glad to see my solution isn’t as big a hack as I thought it was at first.

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

  4. Hi, thanks for the post.
    The seciton list is very usefull in my case but i have a question.
    What determine the height of each row? I’m searching a way to fix the height of each row to it’s content in order to display more elements on the screen.
    I tried lots of stuff but nothing worked, every row has the same height, even if i set the fontsize to 1 the row have the same size.
    any idea?

  5. Nice component, but, it doesn’t have “sticky” headings, i.e. when scrolling it doesn’t keep the last section title on top, and slide the new one as you scroll up/down.

    This is pretty basic for any section title component (see the iphone).

  6. Im using this with a checkbox and button control along with the List Item, I am stuck with the label for the item (label) extending off the right side. Any guidance on how to constraint the normalLabelField width to fit with the other components?

    Thanks!

Leave a Reply

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