Loading and Tracking Multiple Files at the Same Time (Part 2)

ActionScript 2.0

Three weeks ago, I started a two-part series on using a MovieClipLoader instance in a custom class to track the loading of multiple external files at the same time. When we left off, our custom MultiLoader class was complete enough to provide a decent amount of convenience:  an addClip() method managed a specified external file, a target movie clip, and arbitrary event handlers; a loadClips() method set the mass download in motion.  In the final paragraph, I said I’d follow up with a way to report the total group load progress.  This is that follow-up. 

The new stuff

The new functionality is essentially the result of the MovieClipLoader.getProgress() method and the EventDispatcher class, used to dispatch a custom onGroupProgress event.  Because the full MultiLoader class was described last time, I’ll go into the changes first, then present the updated class at the end.

It makes sense to use the existing MovieClipLoader.getProgress() method to gather data on how many bytes have loaded for each file.  Using that method means we can stay clear of the events for each MovieClipLoader instance in our _loaders array … after all, our class allows those events to be associated to functions from outside code, so they’re “already taken.” The problem with MovieClipLoader.getProgress() is that it incorrectly reports the bytesTotal property of its return object during the early stages of a file’s loading.  An incoming JPG might weigh 28,342 bytes, but during its first few milliseconds, when only 218 bytes may have arrived, the getProgress() method reports 218 (or thereabouts) as the total number of bytes.  After repeated testing, I wasn’t able to find a pattern to this value, which stumped me for a while.

Traditionally, we would want to compare bytesLoaded to bytesTotal in a loop to see if the former has reached the value of the latter.  Under the circumstances, however, the value of bytesLoaded was consistently equal to bytesTotal for the first several fractions of a second, which meant I had to find a way to ignore this apparent “equality” until enough of the file had loaded.  My solution — perhaps not the best — was to use an arbitrary _falseTotal value to do the ignoring.

When loadClips() is called, it now sets in motion a setInterval() loop as the last thing it does.  This loop calls a new method, getGroupProgress() every 50 milliseconds.  Here’s how this method looks:

private function getGroupProgress():Void {
  var groupLoaded:Number = 0;
  var groupTotal:Number = 0;
  var i:Number = 0;
  var len:Number = _clips.length;
  for (i = 0; i < len; i++) {
    groupLoaded += _loaders[i].getProgress(
      _clips[i].target
    ).bytesLoaded;
    groupTotal += _loaders[i].getProgress(
      _clips[i].target
    ).bytesTotal;
  }
  if (_falseTotal) {
    if (groupLoaded <= groupTotal) _falseTotal = false;
  } else {
    dispatchEvent(
      {
        type:"onGroupProgress",
        target:this,
        groupBytesLoaded:groupLoaded,
        groupBytesTotal:groupTotal
      }
    );
    if (groupLoaded >= groupTotal) clearInterval(_id);
  }
}

What’s going on?  By default _falseTotal is set to true:  we’re assuming the first few reports of bytesTotal will be incorrect.  Hold that thought.  Two local variables, groupLoaded and groupTotal, are declared and initialized to zero.  A for loop is set up just like in the loadClips() method; this time, however, the loop simply gathers the combined total of the return value of getProgress() as invoked on each MovieClipLoader instance in the _loaders array (see the previous article if this doesn’t immediately make sense).

The getProgress() method works like this:

A MovieClipLoader instance is used to invoke the method, and the corresponding target MovieClip instance is passed in as a parameter.  Under normal circumstances, that might look like this:

mcl.getProgress(myClip);

… but in this case, the sample mcl and myClip instances are being supplied by array elements.

This method returns an object with two properties, bytesLoaded and bytesTotal.  One way to gather those properties is to create an Object instance and set it to the return value …

var progress:Object = mcl.getProgress(myClip);
trace(progress.bytesLoaded);
trace(progress.bytesTotal);

… but a shortcut approach means we can do without the extra object:

trace(mcl.getProgress(myClip).bytesLoaded);
trace(mcl.getProgress(myClip).bytesTotal);

Once these data are gathered, the method checks if _falseTotal is true.  By default it is, so it checks if groupLoaded is less than or equal to groupTotal.  When that finally happens — because enough of any of the loading files has come in that the bytesTotal value reports correctly — _falseTotal is set to false and no longer needed.  (Why less than or equal to? In cases where the files have already been cached, groupLoaded will be equal to groupTotal from the start, but we still need a way to turn off falseTotal. [Thanks to Cezet for helping me come to this realization.])

Next time getGroupProgress() is called by setInterval(), it goes straight into a call to the dispatchEvent() method (event dispatching is covered in this earlier blog entry).  Finally, if groupLoaded is greater than or equal to grouptTotal, the setInterval() loop is cleared.

Pay careful attention to the additions to the class.  We now have private _id and _falseTotal properties, in addition to the three Function properties required by EventDispatcher.  We now have the import statement for EventDispatcher, as well as the initialize() method in our class’s constructor (also the initialization of _falseTotal to true).  Finally, the new getGroupProgress() method.

Using the updated class

Usage can be identical to what it was before.  The only difference may be if you want to handle the new onGroupProgress event, as dispatched by the class.

var ml:MultiLoader = new MultiLoader();
ml.addClip("file1.jpg", clipA);
ml.addClip("file2.jpg", clipB);
ml.addClip("file3.jpg", clipC);
ml.loadClips();
var listener:Object = new Object();
listener.onGroupProgress = function(evt:Object):Void {
  trace(evt.groupBytesLoaded + "/" + evt.groupBytesTotal);
}
ml.addEventListener("onGroupProgress", listener);

The full code for the updated class

import mx.events.EventDispatcher;
class MultiLoader {
  // PROPERTIES
  private var _clips:Array;
  private var _loaders:Array;
  private var _id:Number;
  private var _falseTotal:Boolean;
  private var dispatchEvent:Function;
  public var addEventListener:Function;
  public var removeEventListener:Function; 
  // CONSTRUCTOR
  public function MultiLoader() {
    EventDispatcher.initialize(this);
    _clips = new Array();
    _loaders = new Array();
    _falseTotal = true;
  };
  // METHODS
  // Add Clip
  public function addClip(
    url:String,
    target:MovieClip,
    handlers:Object
  ):Void {
    _clips.push(new Object());
    var current:Number = _clips.length - 1;
    _clips[current].url = url;
    _clips[current].target = target;
    _clips[current].handlers = handlers;
  }
  // Load Clips
  public function loadClips():Void {
    var i:Number = 0;
    var len:Number = _clips.length;
    var prop:String = "";
    for (i = 0; i < len; i++) {
      var listener:Object = new Object();
      _loaders[i] = new MovieClipLoader();
      for (prop in _clips[i].handlers) {
        listener[prop] = _clips[i].handlers[prop];
      }
        _loaders[i].addListener(listener);
        _loaders[i].loadClip(
        _clips[i].url,
        _clips[i].target
      );
    }
    _id = setInterval(this, "getGroupProgress", 50);
  }
  private function getGroupProgress():Void {
    var groupLoaded:Number = 0;
    var groupTotal:Number = 0;
    var i:Number = 0;
    var len:Number = _clips.length;
    for (i = 0; i < len; i++) {
      groupLoaded += _loaders[i].getProgress(
        _clips[i].target
      ).bytesLoaded;
      groupTotal += _loaders[i].getProgress(
        _clips[i].target
      ).bytesTotal;
    }
    if (_falseTotal) {
      if (groupLoaded <= groupTotal) _falseTotal = false;
    } else {
      dispatchEvent(
        {
          type:"onGroupProgress",
          target:this,
          groupBytesLoaded:groupLoaded,
          groupBytesTotal:groupTotal
        }
      );
      if (groupLoaded >= groupTotal) clearInterval(_id);
    }
  }
}

Thanks to Mike for correcting a typo in my code!

Leave a Reply