Saving scroll position between views in a mobile Flex Application

In a mobile Flex application when you navigate to a new view the old view is destroyed. If you then go back to that first view then it is re-instantiated. If you have a List in that initial view then any scroll position changes the user may have done will be lost. This post demonstrates how to save and restore that scroll position when navigating between views.

When pushing a new View on to a ViewNavigator the old View object is destroyed to save memory. However the View has a data property that is not destroyed. This allows you to keep some information around while navigating between views. When the viewDeactivate event of the View is fired then you will want to save the current scroll position to this data object. Then on the creationComplete event of the List restore that saved position. Here is a sample View that demonstrates this code:

<s:View xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
        title="Hockey Players"
        viewDeactivate="saveScrollPosition()">
 
    <fx:Script>
        <![CDATA[
            private function restoreScrollPosition():void {
                // the data might be null if it has never been set
                if (data == null)
                    return;
 
                var restoredVSP:Number = data.verticalScrollPosition as Number;
                var restoredHSP:Number = data.horizontalScrollPosition as Number;
 
                var maxVSP:Number = myList.dataGroup.contentHeight - myList.dataGroup.height;
                var maxHSP:Number = myList.dataGroup.contentWidth - myList.dataGroup.width;
 
                // restore the saved scroll position, but don't set it to higher than the maximum
                // to prevent from orientation changes causing a scroll into excess space
                myList.dataGroup.verticalScrollPosition = Math.min(maxVSP, restoredVSP);
                myList.dataGroup.horizontalScrollPosition = Math.min(maxHSP, restoredHSP);
            }
 
            private function saveScrollPosition():void {
                // first time the data might not be created yet
                if (data == null)
                    data = new Object();
 
                // save the vertical scroll position
                data.verticalScrollPosition = myList.dataGroup.verticalScrollPosition;
                data.horizontalScrollPosition = myList.dataGroup.horizontalScrollPosition;
            }
        ]]>
    </fx:Script>
 
    <s:List id="myList" height="100%" width="100%" labelField="playerName"
            creationComplete="restoreScrollPosition()"
            change="navigator.pushView(views.DetailsView, myList.selectedItem)">
        <s:ArrayList>
            <fx:Object playerName="Craig Anderson" games="72" goals="12" assists="23" />
            ...
        </s:ArrayList>
    </s:List>
</s:View>

Here is a sample Flex Mobile Project that demonstrates this code:

Click on a player’s name in the List in the first view to go see more details about that player. If you then press the back button notice that the scroll position of the List in the first view is restored.

Using an asynchronous dataProvider

The example above demonstrates how to restore the scroll position when the dataProvider is provided in MXML. If your dataProvider arrives asynchronously then you will need to approach this slightly differently. You will need to call restoreScrollPosition() after the first updateComplete event after the dataProviderChanged event. Add the following event handler to your List’s preinitialize event to get this behavior:

private function myList_preinitializeHandler(event:FlexEvent):void {
    var dataProviderChangedHandler:Function = function(e:Event):void {
        myList.addEventListener("updateComplete", updateCompleteHandler);
    }
 
    var updateCompleteHandler:Function = function(e:Event):void {
        restoreScrollPosition();
 
        // remove the event listeners
        myList.removeEventListener("updateComplete", updateCompleteHandler);
        myList.removeEventListener("dataProviderChanged", dataProviderChangedHandler);
    }
 
    myList.addEventListener("dataProviderChanged", dataProviderChangedHandler);
}
 
...
 
<s:List id="myList" height="100%" width="100%" labelField="playerName"
    change="navigator.pushView(views.DetailsView, myList.selectedItem)"
    preinitialize="myList_preinitializeHandler(event)" />

Note: This sample requires Flash Builder “Burrito” and the latest Flex SDK “Hero”. You can download these builds from Adobe Labs.

16 thoughts on “Saving scroll position between views in a mobile Flex Application”

  1. I solved it. After setting the dataProvider add a callLater(restoreScrollPosition) and you will reset the value even if the dataProvider is set at a different timing.

  2. @Jonathan Campos – In that case you might need to wait for the first updateComplete on the List after the dataProvider is changed before setting the scroll position.

  3. This doesn’t seem to work with a list that receives its contents asynchronously. It looks like it only works with static items.

  4. @JJ – I have updated the post to demonstrate how to restore the scroll position when the dataProvider comes in asynchronously by waiting until the first updateComplete after the dataProviderChanged event.

  5. There are issues when trying to use this with paging data that is retrieved via a service call. It will always show the top of the list instead of scrolling to the appropriate area. Could this be because there is an event handler that is missed?

    Also, there is an issue when trying to assign properties to the “data” object as it belongs to the View class and it may already be occupied by a valueObject that is passed into that view when using navigator.pushView(). Would it be more appropriate to use the PersistenceManager in this scenario?

  6. @JJ – Interesting. I haven’t worked with paged data very much, but maybe when a new page comes in the dataProviderChanged event is being fired? If that is the case then you will only want to set the initial scroll position after the first dataProviderChanged event.

    You could probably use the PersistenceManager if you wanted, but it might be more work to keep track of which View instance has which scroll position. Personally I find it easier to work with the data object of the View.

  7. I just updated this post with a new FXP for the latest build of Flash Builder Burrito and fixed a bug where the scroll position gets set too large and can sometimes expose excess space. This can happen when the size of your List gets bigger when returning to that view, for example starting in landscape orientation and returning in portrait orientation.

  8. Thank you Steven.
    Good article. I thought that all that was needed for this to be preserved was setting the persistNavigatorState to true.

  9. That should be used as static method in every View-Component:

    public static function restoreScrollPosition(target:View, group:Group):void
    {
    // the data might be null if it has never been set
    if (target.data == null)
    return;

    var restoredVSP:Number = target.data.verticalScrollPosition as Number;
    var restoredHSP:Number = target.data.horizontalScrollPosition as Number;

    var maxVSP:Number = group.contentHeight – group.height;
    var maxHSP:Number = group.contentWidth – group.width;

    // restore the saved scroll position, but don’t set it to higher than the maximum
    // to prevent from orientation changes causing a scroll into excess space

    group.verticalScrollPosition = Math.min(maxVSP, restoredVSP);
    group.horizontalScrollPosition = Math.min(maxHSP, restoredHSP);
    }

    public static function saveScrollPosition(target:View, group:Group):void
    {
    // first time the data might not be created yet
    if (target.data == null)
    target.data = new Object();

    // save the vertical scroll position

    target.data.verticalScrollPosition = group.verticalScrollPosition;
    target.data.horizontalScrollPosition = group.horizontalScrollPosition;
    }

  10. Since I use variable height items the proposed function did not work.
    I had to go this way:

    Store the current pos:
    var layout:VerticalLayout = list.layout as VerticalLayout;
    _firstIndexInView = layout.firstIndexInView;
    var fraction:Number = layout.fractionOfElementInView(_firstIndexInView);
    _indexOffset = (1 – fraction) * list.dataGroup.getElementAt(list.selectedIndex).height;

    restore the current pos:
    list.ensureIndexIsVisible(_firstIndexInView);
    list.layout.verticalScrollPosition += _indexOffset;

Comments are closed.