«

»

Mar 01

The woes of Frame Animation on Android [w/ code]

My adventures of getting frame animation on the Android 2.1 continue, and take a turn for the worse. Will I come up victorious in the end? Not sure...

Using Android's Frame Animation API

The first attempt I took at frame animation was using Android's own AnimationDrawable. I thought it would give me the best solution, as it's the closest to the native OS and probably optimized. I was wrong.
This API is highly suspect to OutOfMemory exceptions, either when loading an animation of more than ~50 frames, an when loading more than one animation. On top of that, it is not doing a great job at displaying the frames, and produces a lot of jitter.

So using it is very very simple, and I wrote about it in a previous post, when I was still trying to make it work.

Basically, all you need is an ImageView to display your animation. Prepare some AnimationDrawable in XML. Then you can either pre-load an animation, or just fire an animation regularly, which is only setting the Drawable for the ImageView.

If you fire the animation more than once, remember to reset it.

This is by far the simplest way to go, and best if you have a simple animation. But it dies very quickly of memory issues if you push it too hard.

Using HTML and animated GIFs

This seemed like a classical solution for frame animation. The browser should have absolutely no problems playing it - so I thought. Turns out Android 2.1's web browser doesn't play animated GIFs! So when I was working on my development phone, a 2.2er, there was no problem, but when I switched to my deployment phone, a 2.1er , the screen just goes black.
If you're on 2.2 - this a very nice way to frame animate. It's clean and works at high frame rates.

First you would need to obtain animated GIFs that are compatible with Android's web browser. I did that using ImageMagick's (IM) animation toolbox.

convert myanim_split_*.png myanim.gif

But... there are some details to attend to. First, you probably would like to have control over looping the animation, that can be done using the -loop parameter of convert, setting it to 1 will play the animation once, 0 will loop forever.
How about transparent background? this is very important for character animation, that usually live in a "world" and should not occlude the background. Well, if your animation's PNGs have the background transparet, that would reflect in the animated GIF. But, just creating a GIF out of your PNGs will give you something like this:

The background is not clearing frame-to-frame. So add in the -set dispose background (or "dispose previous" to the IM line:

convert myanim_split_*.png -set dispose background myanim.gif

Now it looks better, and also has a transparent background:

There's still the issue of showing it up as part of the layout, and that will be done with WebView. The tricky part is controlling when the animation will fire. There are several ways to go about it, such as loading a single HTML file and using webview.loadUrl with a "javascript:..." URL to replace the image, or using a simpler method of webview.loadData with HTML code that just displays the image with <img src="..">. There is no "right way" as this is the "hack way" anyway.

WebView wb = (WebView) findViewById(R.id.webview);
wb.loadDataWithBaseURL(
		"fake://lala",
		"<div style=\"text-align: center;\"><IMG id=\"myanim\" SRC=\"file:///android_asset/myanim.gif\" style=\"height: 100%\" /></div>", 
		"text/html",  
		"UTF-8", 
		"fake://lala");

Notice that the GIF file must reside in the "assets" directory of the android project, and the "file:///android_asset/" URL goes right to there. You may as well skip the whole HTML thing and just have WebView loadURL the GIF file right away.

Now this will work on Android 2.2 and up, but will fail for Android 2.1 as the web browser doesn't implement animated GIFs yet.

Using MediaPlayer and animated GIFs

I saw somewhere that even though the WebView in 2.1 cannot play GIFs, the MediaPlayer sure can, so it's worth mentioning that.

So I set up a surface in the layout

<SurfaceView android:id="@+id/mysurfaceview"
	android:layout_width="fill_parent" 
	android:layout_height="0dip"
	android:layout_weight="1"
	/>

Got the holder in code, and instantiated a MediaPlayer

mCharPreview = (SurfaceView) findViewById(R.id.mysurfaceview);
holder = mCharPreview.getHolder();
holder.addCallback(this);
extras = getIntent().getExtras();

mMediaPlayer = new MediaPlayer();
mMediaPlayer.setDisplay(holder);
mMediaPlayer.setOnCompletionListener(this);
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnBufferingUpdateListener(this);
mMediaPlayer.setOnVideoSizeChangedListener(this);
mMediaPlayer.setAudioStreamType(AudioManager.);

And then listen on surfaceCreated to prepare the player

public void surfaceCreated(SurfaceHolder holder) {
        try {
            AssetFileDescriptor openFd = getAssets().openFd("myanim.gif");
		mMediaPlayer.setDataSource(openFd.getFileDescriptor());
            mMediaPlayer.prepare();
	} catch (Exception e) {
		e.printStackTrace();
		(new AlertDialog.Builder(this)).setTitle("Exception").setMessage(e.getClass().getName() + ":" + e.getLocalizedMessage()).create().show();
	}
}

Then listen on prepared to start the animation

public void onPrepared(MediaPlayer mediaplayer) {
        mIsVideoReadyToBePlayed = true;
        if (mIsVideoReadyToBePlayed) {
             holder.setFixedSize(200, 300);
             mMediaPlayer.start();        
        }
}

But - this code failed for me in the mMediaPlayer.prepare() with an exception like "Prepare failed.: status=0xFFFFFFF". I'm guessing that the player fails because the format is not recognized.

Using HTML and Javascript

So, after being exhausted with having some engine play the animation, I decided to go back to using WebView with Javascript code to flip the images one after the other.

Well this was pretty straight forward, I created a small HTML code:

<html>
 <script>
 //---------------------------------------------------------------
//just a function to pad numbers with 0s, good for animation frames in sequential files...
 function FormatNumberLength(num, length) {
    var r = "" + num;
    while (r.length < length) {
        r = "0" + r;
    }
    return r;
}
//---------------------------------------------------------------

  
  var state=0;
  var myinterval = -1; //JS interval handler
  
//---------------------------------------------------------------
//parse querystring - that's where I get the image to show, and other parameters
  var qs = (function(a) {
    if (a == "") return {};
    var b = {};
    for (var i = 0; i < a.length; ++i)
    {
        var p=a[i].split('=');
        b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
    }
    return b;
  })(window.location.search.substr(1).split('&'));
//---------------------------------------------------------------
	
  var firstFrame = parseInt(qs["first"]);   //the index of the first frame
  var lastFrame = parseInt(qs["last"]);    //the index of the last frame
	
//---------------------------------------------------------------
  // intializes animation timer
  function ini() {
  	if(firstFrame >= 0) { //animation
    	setTimeout("myinterval = setInterval(\"periodic()\",100);",100);
    } else {				//static
    	document.getElementById("myimg").src="file:///android_asset/"+ qs["anim_file"] +".png";
    }
  }
//---------------------------------------------------------------

  // called regularly to perform animation  
  function periodic() {
    state+=2;  //jump by 2 frames? can change this to 1 if you like...
    animState = firstFrame + state;

    if (animState <= lastFrame) {
		document.getElementById("myimg").src="file:///android_asset/"+ qs["anim_file"] + FormatNumberLength(animState,4) +".png";
    }
    else {
    	if(qs["loop"] == "false")  //'loop' will say if we keep playing the animation.. duh
    		clearInterval(myinterval);
    	else 
    		state = 0;
    }
  }

 </script>
 <style>
 #wrapper {
     text-align: center;
     background-color: black; /* make sure to use this, since sometimes there are ghosts */
 }
 #im {
     background-color: black;
 }
 </style>
 <body onLoad="ini()">
  <div id="wrapper">
     <img id="myimg" src=""/> <!--   this is where the image goes -->
  </div>
 </body>
</html>

Put this file, as usual, in the "assets" directory where it can be found easily with the "file:///android_assets/..." URL.

So, this HTML is loaded into the WebView with some parameters on the URL that will tell it what to show, like so:

       //This class holds all we need for an animation: filename, number of frames, etc.
	private class MyAnim {
		String filename; 
		int start;
		int end;
		boolean loop;
		public MyAnim(String filename, int start, int end) {
			super();
			this.filename = filename;
			this.start = start;
			this.end = end;
			this.loop = false;
		}
		public MyAnim(String filename, int start, int end, boolean loop) {
			super();
			this.filename = filename;
			this.start = start;
			this.end = end;
			this.loop = loop;
		}
	}

        //this function will "fire" an animation, essentially load the HTML code with the proper parameters
	private void fireAnimation(final MyAnim myAnim, final boolean shouldTurn) {
		findViewById(R.id.webview).post(new Runnable() {
			@Override
			public void run() {
				long now = (new Date()).getTime();
				if((now - anim_start_ts) < 2000) return; //let other animations finish man! geez...
				
				WebView wb = (WebView) findViewById(R.id.webview);

				//supply the "base" filename, the first frame number, last frame and should the animation repeat
				wb.loadUrl("file:///android_asset/animate.html?anim_file="+myAnim.filename+"&first="+myAnim.start+"&last="+myAnim.end+"&loop="+myAnim.loop);
				wb.invalidate();
				
				anim_start_ts = now;
			}
		});
	}

The filenames should be sequential, like: "myanim0001.jpg", "myanim0002.jpg", ....
And then an animation may be from the file "mayanim0023.jpg" to "myanim0046.jpg". The parameters for the HTML code will be then: anim_file="myanim"&first=23&last=46.

But! We are not done.
Because stupid Android 2.1 does not clear the first image that loads into the <img ... >!
So you get these weird "ghosting" effects, where the new images of the animation are shown superimposed on the first image...
I couldn't get past this, so I found another way of doing it....

Using HTML, JS and innerHTML

This is what I ended up using.
Basically everything stays the same, except for the fact that now we are not leaving the <img ... > in there to ghost stuff up, we're completely replacing it with new HTML code using innerHTML.

The change is slight, in the JS I used:

    	document.getElementById("wrapper").innerHTML=
	    	"<div style=\"-webkit-transform: scaleX("  +  ((qs["flip"]=="0")?"-1":"1")  +  ");\">"+
	    	"<img id=\"im\" style=\"height:100%\" src=\"file:///android_asset/"+ qs["anim_file"]+"\" />"+
	    	"</div>";

Note that now I also introduced a new parameter: "flip", that uses -webkit-transform to flip the displayed image.

Phew, that was a battle. I won. And now we learned a few things about Android and options for frame animation.
Some other options to explore: flipping OpenGLES textures, encoding animations to mp4s?

Please share your experience with Android frame animation!

Roy.

Portions of this page are modifications based on work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.

Share
  • http://nologo.co.uk pils

    if you looked at my site, you'll see I'm a designer, not an animator, not a scriptor (good word huh).

    I was looking at (more like glancing in comparison to what you've done) the possibility of creating 'ontouch' wallpapers that then do a short character/object animation. I created somesuch with OwnSkin (using bl##dy .gifs). I played with AppInventor. I mucked about with the SDK/Eclipse and broke things...
    Can you give a full example in code with explanation? I may be being thick, but the above conclusion is going in but how to set this up (copy it) is NOT.

    Regardless, I'm EXTREMELY grateful for reading the above and can now have a cup of tea without thinking I'm going mad...hopefully.

    kindest

    pils

  • http://www.apachejava.blogspot.com Aizaz

    You are really a good person and wrote a very marvelous post. Yeah Android is giving really hard time for simple animations although I thought it would be really simple but this is shit.

  • Matt

    I couldn't agree with you more Ajzaz, Droid's animation engine is just complete fucking garbage. That thing is worthless.