Archive for June, 2012

Android Images With Clickable Areas – Part 1

I want to display images in Android and have different clickable regions within the image. I have read about two different ways of doing this.  The first is a bit like image maps in HTML. You define areas on the screen, using a list of coordinates, and connect the areas to actions. The second method  involves overlaying a hotspot image on top of the real image being displayed. The overlay image has exactly the same size as the first image. It uses different colored regions  to indicate the hotspots. When the user clicks on the primary image, code runs to check the pixel at the corresponding point in the hotspot image. In this, the first of two articles about image maps in Android, I will explain what I learned for the second technique.

Let’s start with what the app looks like when you touch the screen. Circles appear that indicate roughly where the clickable regions on the screen are. When you touch one of the regions, the image changes.

For example, when you touch the image near the end of the space ship , the app changes to make it look like you started the space ship.

Touching the image a second time, returns you to the initial view of the app, which is the first picture without the orange circles showing.

The way this app is set up with a FrameView and ImageView gives you an easy way to handle scaling. I used a 800 x 480 image for this demo app. It scales automatically to all screen sizes, and it is high enough resolution that it looks good on a tablet. Here’s what it looks like in a portrait orientation.

In a real app, you might disallow portrait orientation because it has a very small usable view. I left it in for testing reasons. I wanted to know that the image and the hotspot image were scaling correctly.

How It Works

Most of what goes on in this app is straightforward stuff in Android: clickable images, changing the drawable of an ImageView, etc. The really interesting part for me was getting the scaling done automatically. Two parts of that were tricky: (1) getting the layout just right so the images would scale correctly on all Android devices and tablets; (2) getting the clickable regions to work and also to scale automatically.

Two images are used in the main layout for this app. They are shown below. The first is the view of the starbase that you have seen above. The second image has several rectangles on it. If you were to overlay the second image on top of the first, you would see that they are close to the stops were the circles were drawn to indicate clickable regions.

The overlay image has exactly the same size as the first image. It uses different colored regions  to indicate the hotspots. When the app is running, the overlay image is used to locate a hotspot, which is a region on the screen that you want to define an onClick handler for. When the user clicks on the primary image, code runs to check the pixel at the corresponding point in the hotspot image. I will explain that a bit later. First let’s look at how the images are laid out on the screen.

The main layout for this app uses a FrameView. It is defined to fill up the entire screen. Within it are two image views. Views defined this way in a FrameView share the space of the frame. The second one appears on top of the first one.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/my_frame"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:background="@color/background" >
<ImageView 
 android:id="@+id/image_areas"
 android:layout_width="fill_parent" 
 android:layout_height="fill_parent" 
 android:scaleType="fitCenter"
 android:visibility="invisible"
 android:src="@drawable/p2_ship_mask" 
 />
 <ImageView
 android:id="@+id/image"
 android:layout_width="fill_parent" 
 android:layout_height="fill_parent" 
 android:scaleType="fitCenter"
 android:src="@drawable/p2_ship_default"
 />

</FrameLayout>

Notice that the image view that shows the masks image is invisible. When the app runs, you do not see it, but the layout manager has included it in the layout. Since both ImageViews use “fill_parent” (same as “match_parent”), they expand to fill the parent view. Scaling is done according to the rules for “fitCenter”, which means centering the image both horizontally and vertically and scaling until either the width or the height matches the parent view’s dimension.

With this layout in place, all that is needed is code in the Activity to handle the screen being touched. The main activity implements View.onTouchListener. The method required by that interface is the onTouch method. Here is that method, shortened a bit to make it easier to explain.

public boolean onTouch (View v, MotionEvent ev) {
 final int action = ev.getAction();
 // (1) 
 final int evX = (int) ev.getX();
 final int evY = (int) ev.getY();
 switch (action) {
 case MotionEvent.ACTION_DOWN :
   if (currentResource == R.drawable.p2_ship_default) {
   nextImage = R.drawable.p2_ship_pressed;
   } 
   break;
 case MotionEvent.ACTION_UP :
   // On the UP, we do the click action.
   // The hidden image (image_areas) has three different hotspots on it.
   // The colors are red, blue, and yellow.
   // Use image_areas to determine which region the user touched.
   // (2)
   int touchColor = getHotspotColor (R.id.image_areas, evX, evY);
   // Compare the touchColor to the expected values. 
   // Switch to a different image, depending on what color was touched.
   // Note that we use a Color Tool object to test whether the 
   // observed color is close enough to the real color to
   // count as a match. We do this because colors on the screen do 
   // not match the map exactly because of scaling and
   // varying pixel density.
   ColorTool ct = new ColorTool ();
   int tolerance = 25;
   nextImage = R.drawable.p2_ship_default;
   // (3)
   if (ct.closeMatch (Color.RED, touchColor, tolerance)) {
      // Do the action associated with the RED region
      nextImage = R.drawable.p2_ship_alien;
   } else {
     //...
   }
   break;
  } // end switch
  if (nextImage > 0) {
    imageView.setImageResource (nextImage);
    imageView.setTag (nextImage);
  }
  return true;
}

There is a lot going on in this method:

(1) The coordinates of the touch point come from the event object. The coordinates are relative to the view.

(2) Given the coordinates of the touch, we look up the color of a pixel at the corresponding point in the mask image (the one with the colored rectangles). The layout rules for a FrameView and identically sized images ensure that the points correspond. The code for that follows:

public int getHotspotColor (int hotspotId, int x, int y) {
  ImageView img = (ImageView) findViewById (hotspotId);
  img.setDrawingCacheEnabled(true); 
  Bitmap hotspots = Bitmap.createBitmap(img.getDrawingCache()); 
  img.setDrawingCacheEnabled(false);
  return hotspots.getPixel(x, y);
}

(3) The color for point is not necessarily the exact color value we used in the hotspot image. The reason for this is that the colors could change a bit as the image is scaled. We use a new ColorTool object to test for a matching color.

public boolean closeMatch (int color1, int color2, int tolerance) {
 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;
} // end match

That’s all the code parts. Let’s summarize what happens as the app runs.

  • The user touches the screen.
  • Because that Activity has an onTouch handler, the method onTouch gets called.
  • The code there looks at the event argument to get the x-y position of the touch.
  • These are coordinates relative to the origin of the ImageView, which is itself embedded in the FrameView.
  • Given the x-y location of the touch, the code locates the hidden hotspot image and finds the pixel at the corresponding location.
  • It then takes the color there and finds the best match for it in the following colors: WHITE, RED, BLUE, YELLOW.
  • It then takes the action defined for that color.

For this demo, I used a simple image editing program: Mac Paintbrush. I took an image and made a copy so I could see where I wanted to add the clickable regions.  I added the three rectangles for the colors first. Then I added white over everything else. I saved that as PNG file.

Source Code

You can download the source code form the wglxy.com website. Click here: download sources from wglxy.com. The zip is attached to the bottom of that page. After you import the project into Eclipse, it’s a good idea to use the Project – Clean menu item to rebuild the project.

This demo app was compiled with Android 2.3.3 (API 10). It works in all API levels after API 8.

Conclusion

This way of adding clickable regions is easy to do and takes full advantage of Android’s handling of different screen sizes, screen densities, and orientations. You do not incur much overhead for the second overlay image. In my case, it was only 4 KB. The original image, as a png, was about 70 KB. For simple transitions from one activity to another, this method works out pretty well.

In my next note, I will take a look at another method of doing image maps and redo this example.

References

  • Overlay to make parts of image clickable – discussion in the Android Developers group. This is where I learned about the overlay image method.
  • ImageMaps for Android – This is a very good example of doing images maps by defining regions in xml files. It displays a US map where you can touch the different states. Something like this will be the subject of my next article on image maps.
  • freepik spaceship – The spaceship image came from the freepik website. It is free for noncommercial use.

What to do when your Android Emulator will not start from Eclipse

I do Android development work using Eclipse. I work on a MacBook. Like many people doing Android development in Eclipse, I often start an application to run in the Emulator and then see nothing happen. Everything starts out fine. You receive the first few reassuring messages in the console window and then nothing. You start with something like Figure 1 and you get stuck with something like Figure 2.

Figure 1 Starting Your App
(click to enlarge)

Figure 2 – Nothing Shows in the Android Device Chooser

In Figure 2, I show the Android Device Chooser window and Eclipse, waiting to connect. You have the Emulator device window. Here, I show it after I’ve touched the emulated device screen to get it to the home screen. However, notice that you do not see an application running, and no matter how long I wait, it won’t show up. What I used to do is keep starting new devices until I get one that starts up correctly. Occasionally, I would restart all of Eclipse. Lately — let’s say the last six months or so — I have found something that gets Eclipse properly connected to the emulator. Here’s what I do.

I start a Terminal app on my Mac. (I assume that using a CMD window in Windows would also work.) I type the following.

adb kill-server
adb devices

As soon as I issue the second command, the Android Device Chooser comes back to life and detects that indeed there is a device connected. This is shown in Figure 3 below. Also note that my test app continued its start up in the emulated device so there’s now an app running there.

Figure 3 – Killing the ADB server and restarting.

I know this obvious to many people out there. However, a Google search does not necessarily get you to an answer right away. By writing this note, I know I have the answer handy. One of the reasons I write articles on this blog is to make it easy for me to remember things.

The other thing that’s funny about this is that I swear that I have tried this many times in the last couple of years and it did not always work. Maybe I did not notice that it was the one thing that worked reliably, or maybe it did not work all the time until recently. Perhaps it depends on what version of Eclipse and Android Developer Tools (ADT)  and platform tools you have installed. I really don’t know, but thought I’d share my solution in case it works for others. My versions, by the way, are Eclipse Indigo and ADT version 16.0.1.



Follow

Get every new post delivered to your Inbox.

Join 73 other followers