Share on TwitterDigg This

Few weeks ago, I got my self a motorola xoom. From an android developer perspective that is just heaven :). I really believe that iPad is just a giant size iphone, and having the opportunity to have the first real tablet OS is just amazing.

So, last week I’ve started playing around with it for real. I do have some ideas around for an app, that may never took off, but at least I’m having fun creating loads of mini apps and experimenting the new APIs.

Today I’m working with information visualization at Ericsson. And we’ve been trying all types of nice visual paradigms to display information. It’s been a really, really nice ride. One of my favorites is heatmaps. If you want to know how to implement a heatmap this link is your BEST bet. It’s really amazing what the author had achieved. It’s simple, it’s beautiful and it works perfectly. And since “imitation is the sincerest form of flattery” (I’ve just copied this from the best book on REST out there: REST in Practice ). I decided to port the original Javascript and canvas algorithm to the android platform. And on Honeycomb, thanks to the GPU hardware acceleration, it looks quite amazing. So let’s get our hand’s dirty

The alpha map

You should take a time and go to the original article and understand a bit of the magic behind the heatmap. To sum up, first we create a alpha map with a shade of black/white and using alpha to define the heat (intensity) of the future color. That’s really simple with canvas API on android:

RadialGradient g = new RadialGradient(x, y, radius, Color.argb(10, 0, 0, 0), Color.TRANSPARENT, TileMode.CLAMP);
Paint gp = new Paint();
gp.setShader(g);
myCanvas.drawCircle(x, y, radius, gp);

Here we are just creating a circle with a paint configured as a RadialGradient shader. The colors varies from black with a low alpha (almost transparent) to a complete transparent color. The values x,y are just the coordinates we want to add the circles.

Adding some color

Now let’s add some color to the recently created circle. Here what we do is iterate over every pixel of the square that circle is contained within, and change it’s pixel color (keeping the alpha, that’s important). The variable backbuffer is just our bitmap that the canvas we used previously draw at.

private void colorize(float x, float y, float d) {
		if (x + d > myCanvas.getWidth()) {
			x = myCanvas.getWidth() - d;
		}
		if (x < 0) {
			x = 0;
		}
		if (y < 0) {
			y = 0;
		}
		if (y + d > myCanvas.getHeight()) {
			y = myCanvas.getHeight() - d;
		}
 
		int[] pixels = new int[(int) (d * d)];
		backbuffer.getPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
		for (int i = 0; i < pixels.length; i++) {
			int r = 0, g = 0, b = 0, tmp = 0;
			int alpha = pixels[i] >>> 24;
			if (alpha <= 255 && alpha >= 240) {
				tmp = 255 - alpha;
				r = 255 - tmp;
				g = tmp * 12;
			} else if (alpha <= 239 && alpha >= 200) {
				tmp = 234 - alpha;
				r = 255 - (tmp * 8);
				g = 255;
			} else if (alpha <= 199 && alpha >= 150) {
				tmp = 199 - alpha;
				g = 255;
				b = tmp * 5;
			} else if (alpha <= 149 && alpha >= 100) {
				tmp = 149 - alpha;
				g = 255 - (tmp * 5);
				b = 255;
			} else
				b = 255;
			pixels[i] = Color.argb(alpha, r, g, b);
		}
		backbuffer.setPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
	}

What’s worth mentioning on the above code is the extraction of the alpha value from a ARGB integer:

int alpha = pixels[i] >>> 24;

Tracking movement, refreshing the screen

Now the final touch (literally). In android you can track user movement by overriding the onTouchEvent method. So in our View we just override this method, and inside it, we call the method addPoint (which in turn draws the circle, colorize it and refreshes the screen by calling invalidate()) :

public boolean onTouchEvent(MotionEvent event) {
addPoint(event.getX(),event.getY());
return true;
}
public void addPoint(float x, float y){
RadialGradient g = new RadialGradient(x, y, radius, Color.argb(10, 0, 0, 0), Color.TRANSPARENT, TileMode.CLAMP);
Paint gp = new Paint();
gp.setShader(g);
myCanvas.drawCircle(x, y, radius, gp);
colorize(x - radius, y - radius, radius * 2);
invalidate();
}
protected void onDraw(Canvas canvas) {
if (backbuffer == null) {
	init();
}
canvas.drawBitmap(backbuffer, 0, 0, new Paint(Paint.ANTI_ALIAS_FLAG));
}

That’s pretty much it. After adding a point we make a call to invalidate, which will force your view to redraw itself, calling the onDraw method, where you should copy your backBuffer bitmap (which contains your heatmap to your view’s canvas). Besides some initialization code, that’s all that’s needed to create a heatmap on your android. Pretty simple for something so fancy right?

Wait a second, what about multi touch?

Yeah, you are right. There’s no fun doing all this on a device that can track 255 simultaneous fingers right? So the final code below you can see the version with multi touch support.

I really hope you enjoyed this, please let me know if you decide to use this somewhere, it would be quite nice to see that we have smart people and companies out there that would like to benefit from this.

package com.furiousbob.trackme;
 
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
 
public class HeatView extends View {
	private Canvas myCanvas;
	private Bitmap backbuffer;
	private float radius;
 
	public HeatView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}
 
	public HeatView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
 
	public HeatView(Context context) {
		super(context);
	}
 
	private void init() {
		this.radius = 20f;
		backbuffer = Bitmap.createBitmap(getWidth(), getHeight(),
				Bitmap.Config.ARGB_8888);
		myCanvas = new Canvas(backbuffer);
		Paint p = new Paint();
		p.setStyle(Paint.Style.FILL);
		p.setColor(Color.TRANSPARENT);
		myCanvas.drawRect(0, 0, getWidth(), getHeight(), p);
 
	}
 
	@Override
	protected void onDraw(Canvas canvas) {
 
		if (backbuffer == null) {
			init();
		}
		canvas.drawBitmap(backbuffer, 0, 0, new Paint(Paint.ANTI_ALIAS_FLAG));
 
	}
 
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		int fingers = event.getPointerCount();
		float points[][] = new float[fingers][2];
		for (int i = 0; i < fingers; i++) {
			points[i][0] = event.getX(event.getPointerId(i));
			points[i][1] = event.getY(event.getPointerId(i));
		}
		addPoint(points);
		return true;
	}
 
	public void addPoint(float[][] points) {
		for (int i = 0; i < points.length; i++) {
			float x = points[i][0];
			float y = points[i][1];
			RadialGradient g = new RadialGradient(x, y, radius, Color.argb(10,
					0, 0, 0), Color.TRANSPARENT, TileMode.CLAMP);
			Paint gp = new Paint();
			gp.setShader(g);
			myCanvas.drawCircle(x, y, radius, gp);
			colorize(x - radius, y - radius, radius * 2);
		}
		invalidate();
	}
 
	private void colorize(float x, float y, float d) {
		if (x + d > myCanvas.getWidth()) {
			x = myCanvas.getWidth() - d;
		}
		if (x < 0) {
			x = 0;
		}
		if (y < 0) {
			y = 0;
		}
		if (y + d > myCanvas.getHeight()) {
			y = myCanvas.getHeight() - d;
		}
 
		int[] pixels = new int[(int) (d * d)];
		backbuffer.getPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
		for (int i = 0; i < pixels.length; i++) {
			int r = 0, g = 0, b = 0, tmp = 0;
			int alpha = pixels[i] >>> 24;
			if (alpha <= 255 && alpha >= 240) {
				tmp = 255 - alpha;
				r = 255 - tmp;
				g = tmp * 12;
			} else if (alpha <= 239 && alpha >= 200) {
				tmp = 234 - alpha;
				r = 255 - (tmp * 8);
				g = 255;
			} else if (alpha <= 199 && alpha >= 150) {
				tmp = 199 - alpha;
				g = 255;
				b = tmp * 5;
			} else if (alpha <= 149 && alpha >= 100) {
				tmp = 149 - alpha;
				g = 255 - (tmp * 5);
				b = 255;
			} else
				b = 255;
			pixels[i] = Color.argb(alpha, r, g, b);
		}
		backbuffer.setPixels(pixels, 0, (int) d, (int) x, (int) y, (int) d,
				(int) d);
	}
 
}

To give you an idea of this app running, I’ve decided to post a movie :)


Leave a Reply

You must be logged in to post a comment.