Showing Progress for Multiple Loaders
10.17.2009A 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.aaronhardy.utils { import flash.events.Event; import flash.events.EventDispatcher; import flash.events.IEventDispatcher; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.utils.setTimeout; [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. * 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 = []; /** * 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(); } /** * Whether stats are flagged to be updated. */ protected var statsNeedUpdating:Boolean = false; /** * 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; setTimeout(doUpdateStats, 0); } } /** * 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. This 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():void { 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(); dispatchEvent(new Event(Event.COMPLETE)); } // Reset the flag. statsNeedUpdating = false } } } } |
Tags: bytesLoaded, bytesTotal, Loader, LoaderInfo, overall progress, progress indicator, URLLoader


Hi!
im new in as3 and your script it’s just what i was looking for since as2!
I thing that this is for a flex project, im working in pure as3 code, so i cant understand the meta tag
[Event(name="progress", type="flash.events.ProgressEvent")]
[Event(name="complete", type="flash.events.Event")]
it’s like to define a constant for the dispach of the event?
and, u dont have a point of entry like for example:
public function BatchLoaderInfo ():void {
}
how ca i use your utils?
register(loader) for every asset?
thanks so much!