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 spots 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.

About Bill Lahti

Bill Lahti is a software engineer building mobile applications and knowledge management solutions. Two of his interests are writing for this blog and building Android apps, with strategy games being an area of particular interest.
This entry was posted in Android and tagged , , , , . Bookmark the permalink.

37 Responses to Android Images With Clickable Areas – Part 1

  1. Pingback: My Android Programming Tutorials By Topic « More Is Not Always Better

  2. Chitranshu says:

    thoroughly analysed.. and thoroughly explained..
    I got try this for sure.. :)
    thanks..

  3. Lasse says:

    A great and simple implementation. A really nice explanation thank you! When will you be posting the part two? :-)
    Keep up the great work!

  4. Great explanation, so easy to understand, thanks for the valuable information…

  5. Sisko says:

    Superb tutorial and it works perfect with activity. Would it be possible to make it work in fragment?

    • blahti says:

      Yes. This could work in a fragment. What matters is the layout and that layout could be used with a fragment easily. What I have tended to do with fragments is define an interface for the fragment so that the clicks in the views in the fragment become callbacks to the activity that holds the fragment. I do not have any simple examples around. The only thing I have written about fragments is my Horizontal Scrolling article. You could check that and its references and see if that helps you.

  6. david says:

    Many thanks for your blog. Is wonderfoul.
    When will you have the first way published?. If you don’t know, do you know some url or tutorial to can see it.
    Thanks and sorry for my english!

  7. Pingback: Android: Placing button relative to ImageView : Android Community - For Application Development

  8. steve says:

    Any ideas why It doesn’t work when I download and import the project? I’m not new to Java but am new to android so could be me….

    • blahti says:

      A couple of things:
      (1) Check the Project Properties and see which Android it depends on. The original code used 2.3.3. If you do not have that installed, select the one of the versions of Android you have installed.
      (2) On the Project menu, there is a “clean…” item. Run that. Sometimes Eclipse fails to do that automatically for imported code.

      Those are two suggestions, but I should also ask what does “it doesn’t work” mean? Does it mean it does not compile or that it will not run in the emulator?

  9. terence ng says:

    the colors could change a bit as the image is scaled? Is this situation happens in all image format?

    • blahti says:

      You mean format as in png and jpg, right? That sounds right. I have not done a lot with images so I don’t think I can offer much advice.

  10. Pozinux says:

    It’s amazing thanks for this article !
    My problem is that I would like to be able to zoom in this image but I cannot do both at the same time… zoom and touch the clickable area. Do you have any hints for me ?

    • blahti says:

      I looked into different ways to do zooming here: Panning and Zooming.
      I have not tried to combine the that with clickable images. Let me know how that goes for you.

      • Pozinux says:

        Thanks for the quick reply ! I have seen your examples on zooming and the one on touching but I don’t understand how I can mix both of those examples… (I’m preety new to android). If I take this article source code, how can I had simple zoom functionnality ? Here is my OnCreate :

        @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ImageView iv = (ImageView) findViewById (R.id.image);
        if (iv != null) {
        iv.setOnTouchListener (this);
        }

        toast (“Touch the screen to discover where the regions are.”);

        // Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.p2_ship_default);
        // TouchImageView touch = new TouchImageView(this);
        // touch.setImageBitmap(bm);
        // touch.setMaxZoom(4f); //change the max level of zoom, default is 3f
        // setContentView(touch);
        }

        If I uncomment the second part, I can zoom but not touch specific parts anymore…

      • blahti says:

        Here is a suggestion. The Clickable Images example works because you have two images, one overlaying the other. The onTouch listener for that one handles hot spots and makes a click change the image being displayed. The Pan Zoom example that uses PanZoomListener (third one) also makes use of a touch listener. If you want some of the behavior from example 1 and some of the example 2, I think you have to find a way to come up with a new touch listener that can handle both.

        I would suggest starting by modifying the Clickable Images example so that it uses a touch listener subclass rather than calling a method of the Activity. Without changing example more than that, duplicate the existing behavior where the touch causes the image to change.

        Next start thinking about how to combine the two touch listener subclasses into one. Note that each one has its own way of handling touch actions. So the new class has to handle all the events without getting confused about whether it’s doing a pinch zoom or a simple touch. The key to the first example is that the overlay image fields the touch events. The key to the second example is that the touch events are the way it scales the views. So it seems like you have to have extra state variables in your new listener so it knows whether a pinch zoom is happening. When it is, do the zoom logic, but arrange to do it on both views. The Clickable Images demo had two views, but only one needed to have a touch listener. The new one you are doing will need to have both views with touch listeners. Scaling touch events have to be handled the same way for both of those views. Touch events that are just the simple clicks are handled only by the overlay image. Your onCreate method would make sure that both views got a new touch listener assigned to them.

        Generally speaking, with changes like this, I suggest doing it in several small steps. Use Log.d statements in your code to verify things are what they seem. Debugging touch events in the debugger itself is sometimes too confusing because there are so many events.

        I hope this helps you.

      • Pozinux says:

        Thanks blahti ! Your answer helps a lot. I understand the logic and principles of the solution but I already now that my knowledge of programming is too poor to make that happend in my new app unfortunetly… I thought it would be much easier to implement. Too bad for my project. But thanks again for your help. If one day you make a tutorial on mixing both tutorials (zooming and clicking on specific part) let me know ! :-)

  11. I downloaded part of your source file such as the two .java file and all the .png file. It works on my Samsung ACE. Thank you very much. Kent Lau, Malaysia.

  12. Hainizam says:

    Hello sir. Lets say i want to do an apps that have 10 clickable images. How can i click on 3 or 4 images like checkbox? In your in example, i could click only one image. Btw thanks a lot.

    • blahti says:

      In this demo, when you touch a hot spot, a new drawable is brought into the ImageView with a call to setImageResource. You definitely don’t have to do that as your action. I did something simple so I did not have to spend a lot of time setting up images. The key point of this demo was to show a way to make it look like certain areas of a picture are clickable.

      I have one idea you might try. Start with your default image. Construct several images that would be placed on top of the default image. Each of those images would have the part of the image that changes showing and the rest of the image would be transparent. Stack up all your new images inside a FrameView, which makes them something like layers of the picture. All those layers are ImageView pbjects with visibility initially set to “invisible”. When a user touches a hotspot, have your code locate the correct ImageView and sets its visibility to “visible”. As long as your layer images do not overlap, it might have the effect you are after.

      To simplify things, you might start by triggering the changes with regular buttons, just to be sure that the images overlays are right. Then you could switch to driving off the hots spots.

      • Hainizam says:

        I understand what you are doing in this thread but the problem is, I do not know how to convert it to a different process. Lets say, I want to select the alien and the fire. However, the example u posted here show that the user could select only one image. And I dont really get it with the layer things.

        I’m very sorry, this is my first time developing an android games. Everything is still very blur to me. I really hope that someone could explain to me about the concept.

      • blahti says:

        Some suggestions:
        Figure out how a FrameView can be used how to stack one view on top of another. See http://android-developers.blogspot.com/2009/03/android-layout-tricks-3-optimize-by.html for an example.

        Then learn how to build an image with a transparent background. I do not do very elaborate graphics myself, but I know enough about Mac Paintbrush that I can create png files with transparent backgrounds. Just search: how to make transparent background.

        Then do a simple FrameView with a background image and the image with a transparent background.
        Then take what you know to build a more complex background image with several images on top.
        Once you are comfortable with that and can put it in a simple app, I think you will understand what I meant in my comments.

  13. Scott Funkhouser says:

    I am creating my first app ever so admittedly this is all pretty new to me. I am using eclipse and I imported the code from the website and integrated the parts I needed into my app. The app is not registering the hotspot events and as far as I can tell, by inserting log entries in various locations, the app isn’t even getting the evX and evY coordinates, let alone using the ColorTool. This app is just for my own personal use and mostly for learning purposes but it’s very frustrating that I can’t get it to work right. Here’s the code for my main activity
    package stuff.of.mine.golfapp;

    import android.os.Bundle;
    import android.app.Activity;
    import android.graphics.Bitmap;
    import android.graphics.Color;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.ImageView;
    import android.widget.TextView;

    public class MainActivity extends Activity implements View.OnTouchListener{
    TextView textView1, textView2, textView3, textView4;

    int swingCount = 0;
    int hook = 0;
    int slice = 0;
    float goodSwing = 0;
    double accuracy = (goodSwing)/(swingCount);

    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    textView1=
    (TextView) findViewById(R.id.textView1);
    textView2 =
    (TextView) findViewById(R.id.textView2);
    textView3 =
    (TextView) findViewById(R.id.textView3);
    textView4 =
    (TextView) findViewById(R.id.textView4);

    ImageView iv = (ImageView) findViewById (R.id.image);
    if (iv != null) {
    iv.setOnTouchListener (this);

    TextView textView1;
    textView1 =
    ((TextView) findViewById(R.id.textView1));
    textView1.setText(“Swings: ” + swingCount);

    TextView textView2;
    textView2 =
    ((TextView) findViewById(R.id.textView2));
    textView2.setText((“Hooks: “) + hook);

    TextView textView3;
    String test = String.format(“%.02f”, accuracy);
    textView3 =
    ((TextView) findViewById(R.id.textView3));
    textView3.setText(“Accuracy: ” + test + “%”);

    TextView textView4;
    textView4 =
    ((TextView) findViewById(R.id.textView4));
    textView4.setText(“Slices: ” + slice);
    }
    }

    public boolean onTouch (View v, MotionEvent ev)
    {
    boolean handledHere = false;

    final int action = ev.getAction();

    final int evX = (int) ev.getX();
    final int evY = (int) ev.getY();
    int nextImage = -1; // resource id of the next image to display

    // If we cannot find the imageView, return.
    ImageView imageView = (ImageView) v.findViewById (R.id.image);
    if (imageView == null) return false;

    Integer tagNum = (Integer) imageView.getTag ();
    int currentResource = (tagNum == null) ? R.drawable.range_default : tagNum.intValue ();

    switch (action) {
    case MotionEvent.ACTION_DOWN :
    if (currentResource == R.drawable.range_default) {
    nextImage = R.drawable.range_click;
    handledHere = true;

    /*
    } else if (currentResource != R.drawable.p2_ship_default) {
    nextImage = R.drawable.p2_ship_default;
    handledHere = true;
    */
    } else handledHere = true;
    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.
    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.range_default;

    if (ct.closeMatch (Color.RED, touchColor, tolerance)) {hook++; Log.d(“hook”, “hook”);}
    else if (ct.closeMatch (Color.YELLOW, touchColor, tolerance)) goodSwing++;
    else if (ct.closeMatch (Color.BLUE, touchColor, tolerance)) slice++;
    handledHere = true;
    break;

    default:
    handledHere = false;
    } // end switch

    if (handledHere) {

    if (nextImage > 0) {
    imageView.setImageResource (nextImage);
    imageView.setTag (nextImage);
    }
    }
    return handledHere;

    }
    public int getHotspotColor (int hotspotId, int x, int y) {
    ImageView img = (ImageView) findViewById (hotspotId);
    if (img == null) {
    Log.d (“ImageAreasActivity”, “Hot spot image not found”);
    return 0;
    } else {
    img.setDrawingCacheEnabled(true);
    Bitmap hotspots = Bitmap.createBitmap(img.getDrawingCache());
    if (hotspots == null) {
    Log.d (“ImageAreasActivity”, “Hot spot bitmap was not created”);
    return 0;
    } else {
    img.setDrawingCacheEnabled(false);
    return hotspots.getPixel(x, y);
    }
    }

    }

    protected void onSaveInstanceState(Bundle outState){
    super.onSaveInstanceState(outState);
    outState.putInt(“swingCount”, swingCount);
    outState.putInt(“hook”, hook);
    outState.putDouble(“accuracy”, accuracy);
    outState.putFloat(“goodSwing”, goodSwing);
    outState.putInt(“slice”, slice);

    }

    protected void onRestoreInstanceState(Bundle savedInstanceState){
    super.onRestoreInstanceState(savedInstanceState);
    slice=savedInstanceState.getInt(“slice”);
    hook=savedInstanceState.getInt(“hook”);
    swingCount=savedInstanceState.getInt(“swingCount”);
    accuracy=savedInstanceState.getDouble(“accuracy”);
    goodSwing=savedInstanceState.getFloat(“goodSwing”);
    }
    }

    • blahti says:

      One possibility is that you have not made the connection between the view and touch listener in the activity. The code above assumes it can locate the view with id “image”. Is there any chance that your layout file used a different id?

  14. Pingback: Zoom auf Teil eines Bildes - Android-Hilfe.de

  15. Ken says:

    Great blog! I gave this a try and have one question. It seems to work for 4 images but as a post mentioned above not sure how to do more than 4. My eclipse only registers the .color(red,green,blue). Am I missing something to get other .color(black, magenta, …) to work?

  16. Ken says:

    GOT IT TO WORK!!!! super thanks to you, cheers

  17. Excellent tutorial, very helpful, simple and easy to understand.

  18. I have created an app by combining ideas of image with clickable areas and pan/zoom listener for android. Now the image can be zoomed as well as clicked at the same time. I referred this article and this article (http://code.cheesydesign.com/?p=723) to create it. I have explained how I have achieved it in my blog (http://chathura2020.blogspot.com/2014/03/how-to-make-image-with-clickable-areas.html) with the source codes. I hope this will help to others as well.

  19. ARob33 says:

    Hello, I was wondering if I could apply this to an app that I want to make that would allow the user to click on parts of a guitar and do certain actions like make tablature. I would need ~144 hotspots (the number of frets x the number of strings (24×6=144) ) and they would all need to be different colors I assume. Is it even possible using this method? How would you approach this project? Many have reccomended this site as reference but I am having a hard time making it applicable to mine.

    Thanks,
    Aaron Robertson

    • Bill Lahti says:

      Yes. it should be possible but it would be very time consuming to set up for that many hot spots. I doubt that the other method (see the reference: http://catchthecows.com/?p=113) would be much easier. 144 is still a large number. For the method I used, you’d have to come up 144 distinct colors and have the code reliably tell the difference. So that’s one way.

      I wonder if you could get by with 6 colors, one for each of the strings. Then you’d know which string was touched and then use the x touch point to figure out which fret the player was on. If you chose two more unique colors and put them on the first fret and the last fret, you could do a scan of the image one time as that view went on the screen and you’d know how much room 24 frets takes on whatever device the app happens to be running on. Then you could calculate the fret number dynamically from the user touch. Of course, I made a few assumptions there, like the fret being laid our horizontally or vertically so you could find the ends. But it almost seems doable.

  20. Very useful. Thanks!

    I’ve based myself on it to make a clickable map from my region, using a conventional map as “main” image, and another copy of the map with the regions coloured as “mask” image. If the colours in the mask are different enough between them, works perfectly :)

  21. Kevin Hollingshead says:

    Really smart idea to use a hidden color mask! I’ve approached this either by defining touchable rectangular regions (left, top, right, bottom), or by labeling areas with a manually drawn touchable label. Can’t wait until I have a reason to do something with this!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s