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().
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())?
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:
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):
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
// Position the element
Here is the complete code for updateDisplayList() when useVirtualLayout is set to true:
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()):
For the complete code of this layout manager please check the Download the code section of this post.
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.
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).
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
Here is the code:
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:
- 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.
- 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).
- 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.
- 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.
- 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:
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:
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:
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.