This post goes over the basics of writing a very simple custom layout that supports virtualization in spark. It assumes that you have already built a custom layout that handles the real (non-virtual) case and are comfortable doing so.
The main difference between a virtual and non-virtual layout is how many renderers are created. A non-virtual layout creates a renderer for every item at startup, where virtual layouts only create renderers for items that are currently in view. This is a huge performance gain when you have dataProviders with thousands of items, but only a handful are shown at any given time. The spark List turns on virtual layout by default.
The layout we build here is called SimpleHorizontalLayout. As the name suggests, it is a very simplistic approach to something like the spark HorizontalLayout. It extends LayoutBase and lays out elements next to each other from left to right. It assumes that every element is the same size as the typicalLayoutElement (which is the renderer created for the typicalItem). If the typicalItem is not specified by the user then the first item in the dataProvider will be used by default.
Here is a rough outline of what to do when writing a virtual layout:
- start with a layout that works in the real (non-virtual) case
- override measure()
- override updateDisplayList()
- override scrollPositionChanged()
If you want your layout to support keyboard navigation in a spark List then you will also need to:
- override getElementBoundsLeftOfScrollRect() etc.
- override getElementBounds()
- override getNavigationDestinationIndex()
1. Override measure()
In virtual layouts, the measure method should only rely on size data cached at updateDisplayList() time. Everything else (that’s everything the first time measure is called) has to be estimated. In this simple case we guess at the size by assuming that every renderer is the same size as the typicalLayoutElement.
2. Override updateDisplayList()
In the non-virtual case you are able to loop through every element in the target and lay them all out. This means that the target’s contentWidth/contentHeight is calculated exactly. But in the virtual case you don’t have this luxury, you know a couple of things: the scroll position, the size of the target, and the typicalLayoutElement. Given these things you will need to figure out which indices are actually in view and layout only those elements. I like to follow this procedure when tackling this step:
1. Figure out what we’re given
2. Given the scroll position figure out the first index that should be in view
3. Figure out how many indices are in view
4. Figure out the last index in view
5. Figure out what coordinates to position the first index at
6. Position the renderer of each index that is in view using getVirtualElementAt()
7. Keep track of how many pixels visible renderers are hanging off the ends of the view
The important thing about a virtual layout updateDisplayList() method is that it can only retrieve elements with target.getVirtualElementAt() and that it should only retrieve the elements it needs. Also getVirtualElementAt() should never be called outside of the updateDisplayList() in the layout. Currently only DataGroup supports virtual layouts. It does so by allocating the renderers requested by layout.updateDisplayList(), freeing the rest.
3. Override scrollPositionChanged()
When the scroll position changes it might mean that new items are coming into view so we need to invalidate the display list if that is the case. You’ll notice that this sample has an optimization that only invalidates the display list if the scroll position has changed enough to expose new renderers. This is done by taking advantage of the Flash scrollRect and knowing that the renderers that are only partially in view can be valuable in saving extra invalidations.
4. Override getElementBoundsLeftOfScrollRect() etc.
This method returns the bounds of the first layout element that either spans or is to the left of the scrollRect’s left edge. This gets called when someone clicks on the track or the arrows of the List’s scrollbar.
5. Override getElementBounds()
This method returns a Rectangle that corresponds to the layout bounds of the item at the given index. A virtual layout overrides this method to compute a potentially approximate value for elements that are not in view. This works together with getNavigationDestinationIndex() to support keyboard navigation.
6. Override getNavigationDestinationIndex()
This method returns the index that the List should scroll to given a specific keyboard navigation command like HOME, END, LEFT, RIGHT, etc.
You can find the code equivalent to this procedure in the source of SimpleHorizontalLayout.as in the following example:
Note: Item indices don’t necessarily have to be contiguous for a virtual layout. See SDK-24052 for an example.
In this example the virtual layout perfectly approximates it’s non-virtual counterpart. This is because we assumed that every element was equal in size and assumed that size was equal to the typicalLayoutElement’s size. In spark the HorizontalLayout/VerticalLayout classes are more robust in being able to handle renderers of different sizes as long as a reasonable typicalLayoutElement/typicalItem is provided. This is made possible by caching renderer sizes it encounters in order to make as accurate a prediction as possible (see spark.layouts.supportClasses.LinearLayoutVector for the cache implementation). These layouts also provide useful properties like gap, horizontalAlign, verticalAlign, etc. that are beyond the scope of this post.
Here are some ways to learn more about virtual layout in Flex 4:
- Read the Spark Virtualization spec.
- Read the ASDoc for spark.layouts.supportClasses.LayoutBase.
- Look at the code in HorizontalLayout/VerticalLayout.
- Keep an eye on Hans and Evtim‘s blogs.
Note: This sample requires Flex SDK 22.214.171.12429 or higher. You can get the latest SDK builds from opensource.adobe.com.