Developer

How to use particle effects to surprise and delight your Android app users

Android app developers, learn the basics behind particle effects and see a demo of a simple implementation.

happy-group_iStock.jpg

As the number of apps available to smartphone users continues to grow by leaps and bounds, developers are finding they often have to get creative to amass and keep users. In video games, it is not uncommon to see particle effects used to simulate explosions, weather, and all sorts of other eye candy.

Advanced particle engines involve lots of math, the application of physics, and often direct access to an open GL surface, but that doesn't mean we can't achieve reasonable particle effects in our apps using the standard UI framework and a handful of straightforward simulation parameters.

This tutorial is a very simplistic application of a particle effect simulating an explosion. You can follow along or download and import the entire project directly into Eclipse.

1. Create a new Android project in Eclipse. Because the application will be graphic intensive, I'm choosing to only support devices running Android 4.1 (Jelly Bean) or higher.

2. In the /res/drawable-xhdpi folder, I placed two images: a crate (Figure A) and a fireball (Figure B).

Figure A

boxandroid041614.png

Figure B

firefxandroid041614.png

3. In the /res/layout folder, I modified the default layout to consist of a frame layout and a single image view.

activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/frame"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:background="#000000">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/box"
        android:layout_gravity="center"
        android:id="@+id/bomb_img"/>
        
</FrameLayout>

4. Our /src folder will consist of three classes. The first is responsible for defining a single particle in an explosion.

Particle.java
package com.authorwjf.simpleparticlefx;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class Particle {
	
	public static final int ALIVE = 0;	
	public static final int DEAD = 1;		
	private int mState;	
	private Bitmap mBitmap;
	private float mX, mY;			
	private double mXV, mYV;		
	private float mAge;			
	private float mLifetime;	
	private Paint mPaint;	
	private int mAlpha;
	private static Bitmap mBase;
	
	private double randomDouble(double min, double max) {
		return min + (max - min) * Math.random();
	}
	
	public boolean isAlive() {
		return mState == ALIVE;
	}
	
	public Particle(int x, int y, int lifetime, int maxSpeed, int maxScale, Context c) {
		mX = x;
		mY = y;
		mState = ALIVE;
		if (mBase==null) {
			mBase = BitmapFactory.decodeResource(c.getResources(),R.drawable.fire_fx);;
		}
		int newWidth = (int) (mBase.getWidth()*randomDouble(1.01, maxScale));
		int newHeight = (int) (mBase.getHeight()*randomDouble(1.01, maxScale));
		mBitmap = Bitmap.createScaledBitmap(mBase, newWidth, newHeight, true);
		mLifetime = lifetime;
		mAge = 0;
		mAlpha = 0xff;
		mXV = (randomDouble(0, maxSpeed * 2) - maxSpeed);
		mYV = (randomDouble(0, maxSpeed * 2) - maxSpeed);
		mPaint = new Paint();
		if (mXV * mXV + mYV * mYV > maxSpeed * maxSpeed) {
			mXV *= 0.7;
			mYV *= 0.7;
		}
	}
	
	public void update() {
		if (mState != DEAD) {
			mX += mXV;
			mY += mYV;
			if (mAlpha <= 0) {						
				mState = DEAD;
			} else {
				mAge++;	
				float factor = (mAge/mLifetime) * 2;
				mAlpha = (int) (0xff - (0xff * factor));
				mPaint.setAlpha(mAlpha);				
			}
			if (mAge >= mLifetime) {	
				mState = DEAD;
			}
		}
	}
	
	public void draw(Canvas canvas) {
		canvas.drawBitmap(mBitmap, mX, mY, mPaint);
	}

}

5. Our next class, Explosion.java, is simply a collection of particles.

Explosion.java
package com.authorwjf.simpleparticlefx;

import android.content.Context;
import android.graphics.Canvas;

public class Explosion {

	public static final int ALIVE 	= 0;
	public static final int DEAD 	= 1;
	private final static int LIFETIME = 50;
	private final static int MAX_SCALE = 4;
	private final static int MAX_SPEED = 30;
	
	
	private Particle[] mParticles;			
	private int mState;						
	
	public Explosion(int numberOfParticles, int x, int y, Context c) {
		mState = ALIVE;
		mParticles = new Particle[numberOfParticles];
	 	for (int i = 0; i < mParticles.length; i++) {
			Particle p = new Particle(x, y, LIFETIME, MAX_SPEED, MAX_SCALE, c);
			mParticles[i] = p;
		}
	}
	
	public boolean isDead() {
		return mState == DEAD;
	}

	public void update(Canvas canvas) {
		if (mState != DEAD) {
			boolean isDead = true;
			for (int i = 0; i < mParticles.length; i++) {
				if (mParticles[i].isAlive()) {
					mParticles[i].update();
					isDead = false;
				}
			}
			if (isDead)
				mState = DEAD; 
		}
		draw(canvas);
	}
	
	public void draw(Canvas canvas) {
		for(int i = 0; i < mParticles.length; i++) {
			if (mParticles[i].isAlive()) {
				mParticles[i].draw(canvas);
			}
		}
	}
}

6. The final class is our familiar MainAcitvy.java file. Here we wire up the UI, handle callbacks, and reschedule screen paints as required.

MainActivity.Java
package com.authorwjf.simpleparticlefx;

import android.app.Activity;
import android.graphics.Canvas;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.FrameLayout;
import android.widget.ImageView;

public class MainActivity extends Activity implements OnClickListener {

	private final static int NUM_PARTICLES = 25;
	private final static int FRAME_RATE = 30;
	private final static int LIFETIME = 300;
	private Handler mHandler;
	private View mFX;
	private Explosion mExplosion;
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		FrameLayout layout = (FrameLayout) findViewById(R.id.frame);
		final ImageView iv = (ImageView) findViewById(R.id.bomb_img);
		mFX = new View(this) {
			@Override
		    protected void onDraw(Canvas c) {
				if (mExplosion!=null && !mExplosion.isDead())  {
					mExplosion.update(c);
					mHandler.removeCallbacks(mRunner);
					mHandler.postDelayed(mRunner, FRAME_RATE);
				} else if (mExplosion!=null && mExplosion.isDead()) {
					iv.setAlpha(1f);
				}
				super.onDraw(c);
			}
		};
		mFX.setLayoutParams(new FrameLayout.LayoutParams(
				FrameLayout.LayoutParams.MATCH_PARENT,
				FrameLayout.LayoutParams.MATCH_PARENT));
		layout.addView(mFX);
		mHandler = new Handler();
		iv.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		if (mExplosion==null || mExplosion.isDead()) {
			int[] loc = new int[2];
			v.getLocationOnScreen(loc);
			int offsetX = (int) (loc[0] + (v.getWidth()*.25));
			int offsetY = (int) (loc[1] - (v.getHeight()*.5));
			mExplosion = new Explosion(NUM_PARTICLES, offsetX, offsetY, this);
			mHandler.removeCallbacks(mRunner);
			mHandler.post(mRunner);
			v.animate().alpha(0).setDuration(LIFETIME).start();
		}
	}
	
	private Runnable mRunner = new Runnable() {
		@Override
		public void run() {
			mHandler.removeCallbacks(mRunner);
			mFX.invalidate();
		}
	};

}

Run the app on a device or simulator and tap the crate. Boom!

About

William J Francis began programming computers at age eleven. Specializing in embedded and mobile platforms, he has more than 20 years of professional software engineering under his belt, including a four year stint in the US Army's Military Intellige...

Editor's Picks