Showing Progress for Multiple Loaders
-
December 9th, 2009
by Aaron Hardy
A common thing to do in most any rich application is to show progress while loading remote assets. In ActionScript, the bytesLoaded and bytesTotal properties of classes such as LoaderInfo and URLLoader provide the needed information to show a fancy percentage-based progress indicator. The bytesLoaded property is updated as more and more of the asset is loaded in. Divide that by the bytesTotal and you have the percentage loaded.
Easy enough. How about multiple loaders? Lets say you have a group of 10 images and you’d like to show progress for all the images collectively. Because you have 10 different images you also have 10 different URLLoader instances (or Loader instances, whatever suits your fancy)–one for each image. Math would say that you divide the sum of bytesLoaded by the sum of bytesTotal and that gives you the percentage complete. Two issues arise:
(1) The progress indicator really shouldn’t be involved with handling all these loaders and tallying their numbers. Its purpose is to display the percentage loaded and it should stick to what it does best.
(2) One important thing to know about ActionScript classes that expose progress information is that the bytesTotal property is not populated until the server responds to the request (until the HTTP headers are received). This makes things a little more complex. Let’s say the first loader begins loading an image and it gets a few bytes loaded in. At this point we for sure know how many total bytes the loader eventually will have loaded, but the other loaders may still be waiting for a response from the server and therefore their bytesTotal properties are still hanging out at 0. What’s your denominator to determine overall percentage? One option is to know the total number of bytes for each image before the loading process begins. This information would need to be available beforehand through some sort of service call. The more simple and usually sufficient approach is to take the number of completed loaders divided by the total number of loaders to determine the percentage loaded. It’s not as granular as dealing with bytes, but it can be a good alternative for showing overall progress.
Below is a class I use to simplify this process and consolidate different objects that provide progress information. Instances of information-providing classes such as LoaderInfo and URLLoader are registered using the register() method. As the loaders progress, the percentLoaded property is updated and progress events are dispatched. If only a single loader is registered, percentLoaded will reflect bytesLoaded/bytesTotal. If multiple loaders are registered, percentLoaded will reflect loaders complete/loaders total. When all loaders have completed loading, a complete event is dispatched. Enjoy!
package com.mediarain.utils
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.TimerEvent;
import flash.utils.Timer;
[Event(name="progress", type="flash.events.ProgressEvent")]
[Event(name="complete", type="flash.events.Event")]
/**
* A object which consolidates a group of objects that provide progress information. Types
* of objects include UrlLoader and LoaderInfo objects. This allows a simple way for displaying
* overall progress for multiple loaders.
*
* When a single loader is registered, the percentLoaded is simply the bytes loaded divided by
* the bytes total for the loader. When there are multiple loaders registered, percentLoaded
* reflects the number of loaders completed divided by the total number of registered loaders.
* The reason why using multiple loaders is less accurate is because a loader does not know
* how many bytes it will be loading until the first response from the server. Because loaders
* are often queued, this means that we can't determine the total number of bytes to be loaded
* for all registered loaders up front.
*/
public class BatchLoaderInfo extends EventDispatcher
{
[Bindable]
/**
* The percentage of all registered loaders which are loaded.
* When there is a single loader, this is simply the bytes loaded divided by the bytes total
* for the loader. When there are multiple loaders, this is the number of loaders
* which have completed their loading process divided by the total number of registered
* loaders. This value is between 0 and 1.
* @see doUpdateStats
*/
public var percentLoaded:Number;
/**
* @private
* Registered loaders.
*/
protected var loaders:Array = [];
/**
* Timer used to delay updating status till the next frame.
*/
protected var updateStatsDelayTimer:Timer;
/**
* Whether stats are flagged to be updated.
*/
protected var statsNeedUpdating:Boolean = false;
/**
* Registers a loader with the registry.
* @param loader Any object that dispatches ProgressEvent.PROGRESS, Event.COMPLETE, and
* IOErrorEvent.IO_ERROR events as well as provides bytesLoaded and bytesTotal properties.
* Examples include UrlLoader or LoaderInfo objects.
*/
public function register(loader:IEventDispatcher):void
{
if (loaders.indexOf(loader) == -1)
{
loaders.push(loader);
loader.addEventListener(ProgressEvent.PROGRESS, loaderProgressHandler);
loader.addEventListener(Event.COMPLETE, loaderCompleteHandler);
loader.addEventListener(IOErrorEvent.IO_ERROR, loaderErrorHandler);
updateStats();
}
}
/**
* Unregisters a loader from the registry.
* @param loader Any object that dispatches ProgressEvent.PROGRESS, Event.COMPLETE, and
* IOErrorEvent.IO_ERROR events as well as provides bytesLoaded and bytesTotal properties.
* Examples include UrlLoader or LoaderInfo objects.
*/
public function unregister(loader:IEventDispatcher):void
{
if (loaders.indexOf(loader) > -1)
{
loaders.splice(loaders.indexOf(loader), 1);
loader.removeEventListener(ProgressEvent.PROGRESS, loaderProgressHandler);
loader.removeEventListener(Event.COMPLETE, loaderCompleteHandler);
loader.removeEventListener(IOErrorEvent.IO_ERROR, loaderErrorHandler);
updateStats();
}
}
/**
* Unregisters all the currently registered loaders.
*/
public function unregisterAll():void
{
// Use a copy of the loaders array because the original loaders array will be
// manipulated as we loop through removing loaders.
var loaders:Array = loaders.slice();
for each (var loader:IEventDispatcher in loaders)
{
unregister(loader);
}
}
/**
* Updates stats when progress events are dispatched.
*/
protected function loaderProgressHandler(event:ProgressEvent):void
{
updateStats();
}
/**
* Updates stats when complete events are dispatched.
*/
protected function loaderCompleteHandler(event:Event):void
{
updateStats();
}
/**
* Updates stats when error events are dispatched.
*/
protected function loaderErrorHandler(event:IOErrorEvent):void
{
// Because the loader will never reach a complete state because of the error,
// we'll unregister the loader now.
unregister(IEventDispatcher(event.target));
updateStats();
}
/**
* Marks that we need to update stats. We delay the actual updating of the stats
* until the next frame as a type of invalidation/validation cycle like the Flex components
* go through. This is to avoid duplicate calls to the stat updating logic within a
* single render.
*/
protected function updateStats():void
{
if (!statsNeedUpdating)
{
statsNeedUpdating = true;
if (!updateStatsDelayTimer)
{
updateStatsDelayTimer = new Timer(0, 1);
}
updateStatsDelayTimer.reset();
updateStatsDelayTimer.addEventListener(TimerEvent.TIMER_COMPLETE, doUpdateStats);
updateStatsDelayTimer.start();
}
}
/**
* Updates percentLoaded stats. When there is a single loader, this is simply the bytes
* loaded divided by the bytes total for the loader. When there are multiple loaders, this
* is the number of loaders which have completed their loading process divided by the total
* number of registered loaders. The reason we can't go off bytes when there are multiple
* loaders is because a loader's totalBytes is only populated once it has made its initial
* request to the server. Usually requests are queued, which means the third loader might
* not make its initial request until the first request has returned. This means we don't
* know our grand total number of bytes at the beginning, which is imperative to have
* to make an accurate bytesLoaded/bytesTotal calculation.
*/
protected function doUpdateStats(event:TimerEvent):void
{
updateStatsDelayTimer.removeEventListener(TimerEvent.TIMER_COMPLETE, doUpdateStats);
if (statsNeedUpdating)
{
// See the asdoc for this function to understand why we have these different cases.
if (loaders.length > 1)
{
var loadersComplete:uint = 0;
for each (var loader:Object in loaders)
{
// bytesTotal is only populated (greater than 0) once the loader has made
// its request to the server.
if (loader.bytesTotal > 0 && loader.bytesLoaded == loader.bytesTotal)
{
loadersComplete++;
}
}
if (loaders.length == 0)
{
percentLoaded = 1;
}
else
{
percentLoaded = loadersComplete / loaders.length;
}
}
else if (loaders.length == 1)
{
// bytesTotal is only populated (greater than 0) once the loader has made
// its request to the server.
if (loaders[0].bytesTotal > 0)
{
percentLoaded = loaders[0].bytesLoaded / loaders[0].bytesTotal;
}
else
{
percentLoaded = 0;
}
}
else
{
percentLoaded = 1;
}
dispatchEvent(new ProgressEvent(ProgressEvent.PROGRESS));
// If all the loaders we have registered have completed loading, we'll
// unregister all of them and dispatch a complete event.
if (percentLoaded == 1)
{
unregisterAll();
updateStatsDelayTimer = null;
dispatchEvent(new Event(Event.COMPLETE));
}
// Reset the flag.
statsNeedUpdating = false
}
}
}
}
Tags: bytesLoaded, bytesTotal, LoaderInfo, multiple loaders, progress indicator
there is a nice class called QueueLoader that makes loading (and tracking progress of) multiple files extremely easy.
http://blog.hydrotik.com/category/queueloader/
question though, have you ever encountered loading multiple external files at the same time, each file being loaded into a separate instance of a class, and the progress of each of those files is affected by the other loaders? i.e. the progress bars will look like they are almost at 100% but then jump back down to 0 and start loading again… trying to figure out whats going on
Henry,
Thanks for the link. Looks interesting.
In your problem, are you using some sort of batch progress manager? If you’re not at all then I don’t know–I’ve never seen what you’re describing. But if you are batching the loader progress, you may get that effect of the progress bar jumping around if your formula for determining percent loaded is based off the totalBytes reported by all loaders. I explained this a little in the post, but let’s say you have five loaders that all start out with 0 bytesLoaded and 0 totalBytes. The first loader starts loading your external file and it reports something like 900 bytesLoaded out of 1000 totalBytes. The formula for determining the percent loaded evaluates to something like this: (900 + 0 + 0 + 0 + 0) / (1000 + 0 + 0 + 0 + 0). This shows that the progress of all the loaders is 90% which is incorrect. The reason is because the remaining loaders don’t report their totalBytes until they receive an HTTP response from the server. If the second loader received its HTTP response at that moment, your equation might evaluate to something like this: (900 + 100 + 0 + 0 + 0) / (1000 + 4000 + 0 + 0 + 0). This shows that the progress of all the loaders is 20% which is not only incorrect but it would cause a jittering effect similar to what you described.
Thanks for the comment!
I trying long time to reach capacity in loading multiple. Many of them is completion, but not with many events all. Please consideration of each time trying to access totalBytes and bytesLoaded advice. When is best for these issues? In beginnings or after end all?
Thanks big for posting achievement!
Erneartsno
Earneartsno thanks for respond to comment. Do test totalbytes after behind bytesLoaded for good result and reach capacity. Response needs arrive first for achievement.
Happy to code!