Android: Scalable background images with clickable hotmaps

Every once in a while I decide to fire up the Android SDK.

Once I start hitting up the GUI development... I wonder what the hell am I doing it for? It's frustrating.

Anyway whilst working on my Diablo 3 Max Stats app, I stumbled upon a problem. A problem of detecting "hit-boxes" on a scalable background image.

Displaying the image itself isn't too bad, as long as it's centered in the middle of the screen. Adjusting the invisible overlay views on top in the right position, pain in the ass.

So I gave up. Fuck it, why waste my time?

Instead, I stumbled upon a post by Bill Lahti which made me think in a different way. Using old-school Duck Hunt technology to determine my coordinates.

Why not? Worth a shot and it looks quick and easy to implement.

Setting up the layout

  • First, we make a FrameLayout the root for our layout.
  • On the FrameLayout, I set:
    image
  • Then we add an ImageView (img_bg) on it. The FrameLayout automatically scales and centers the ImageView.
  • We add an ImageView rather than setting the "Background" option of the Layout, since we can choose the scaling type.
  • For the ImageView, set the width/height to fill_parent, adjust view bounds to True and scaling type to "centerInside".

image
You should now have something like this (ImageView selected)

  • Now add another ImageView (img_hitbox) into the FrameLayout, but this time choose the "hit-box" image (see below).
  • Important! Set the same settings as the background image. They have to be the same size positioned in the same place for this to work!

Your current structure should now look like this.

image

image

  • Finally but most importantly, set the Visibility of the hitbox to be "invisible". If you select "gone", it won't be added to the UI at all.

That's the WYSIWYG side of things done.

Setting up the hit-box code

Now for some code trickery. Because the img_hitbox ImageView visibility is set to invisible, it can't receive touch events.

So, we have to add the event handler to our Activity instead.

Ideally, what will happen is:

  • User touches screen.
  • Activity receives onTouchEvent() signal.
  • We translate the X/Y of that touch into a pixel colour in the hit-box.
  • Depending on the colour, we do various things.

And here's the source that'll do just that!

public class D3minmaxActivity extends Activity {
@Override
public boolean onTouchEvent(MotionEvent event) {
// We only care about the ACTION_UP event
if (event.getAction() != MotionEvent.ACTION_UP) {
return super.onTouchEvent(event);
}

// Get the colour of the clicked coordinates
// And yes, I spell it coloUr.
int x = (int) event.getX();
int y = (int) event.getY();
int touchColour = getHitboxColour(x, y);

StringBuilder sb = new StringBuilder();
sb.append("ARGB(");
sb.append(Color.alpha(touchColour));
sb.append(",");
sb.append(Color.red(touchColour));
sb.append(",");
sb.append(Color.green(touchColour));
sb.append(",");
sb.append(Color.blue(touchColour));
sb.append(")");

Log.e("Clicked", sb.toString());

// We do this because the pixel colour returned isn't an exact match due to scaling + cache quality
for (Integer col : colorMap.keySet()) {
if (closeMatch(col, touchColour)) {
Log.e("SuCESS!", colorMap.get(col).toString());
Intent data = new Intent(this, ModifiersActivity.class);
data.putExtra("slot", colorMap.get(col));
startActivity(data);
return true;
}
}

// No close matches found
Log.e("clicked", "nothing");
return false;
}

/**
* This is where the magic happens.
* @return Color The colour of the clicked position.
*/
public int getHitboxColour(int x, int y) {
ImageView iv = (ImageView) findViewById(R.id.img_hitbox);
Bitmap bmpHotspots;
int pixel;

// Fix any offsets by the positioning of screen elements such as Activity titlebar.
// This part was causing me issues when I was testing out Bill Lahti's code.
int[] location = new int[2];
iv.getLocationOnScreen(location);
x -= location[0];
y -= location[1];

// Prevent crashes, return background noise
if ((x < 0) || (y < 0)) {
return Color.WHITE;
}

// Draw the scaled bitmap into memory
iv.setDrawingCacheEnabled(true);
bmpHotspots = Bitmap.createBitmap(iv.getDrawingCache());
iv.setDrawingCacheEnabled(false);

pixel = bmpHotspots.getPixel(x, y);
bmpHotspots.recycle();
return pixel;
}

public boolean closeMatch(int color1, int color2) {
int tolerance = 25;

if ((int) Math.abs (Color.red (color1) - Color.red (color2)) > tolerance) {
return false;
}
if ((int) Math.abs (Color.green (color1) - Color.green (color2)) > tolerance) {
return false;
}
if ((int) Math.abs (Color.blue (color1) - Color.blue (color2)) > tolerance) {
return false;
}

return true;
}
}

Best part about this solution is that it's orientation friendly!

Extra consideration

The main issue I had with implementing Bill's solution is that I had the Activity toolbar showing. This caused the getPixel() function to bug out, as it was trying to fetch pixels for coordinates from outside of the bitmap.

I've fixed that issue by determining the location of the ImageView and adjusting it accordingly.

Another difference is that his code had a colour tolerance level. My version wants precise colours. Colour me foolish, I didn't realise until I was testing on the actual device that the pixel colours returned are not exact. I've added the tolerance code back in.

Also fixed a memory leak issue when not recycling bitmaps.

Sources

 
Copyright © Twig's Tech Tips
Theme by BloggerThemes & TopWPThemes Sponsored by iBlogtoBlog