Button with non-rectangular shape

Basic Tutorials concerning: GUI, Views, Activites, XML, Layouts, Intents, ...

Button with non-rectangular shape

Postby Aetius476 » Fri May 27, 2011 5:06 am

Seeing as I myself am a newbie, the following tutorial represents a good chunk of the total time I have spent coding android. Hopefully I can prevent at least one newbie from googling as much as I did trying to solve it:

Issue: You want the clickable area of a button to be a non-rectangular shape, or, as in my case, wish to divide a single image up into multiple clickable areas. My goal was to take the following image and have each button be clickable:
direction_pad.png (87.33 KiB) Viewed 1443 times

The one limitation of this method is that you need to be able to define your clickable area mathematically (bust out that old algebraic geometry from middle school, we're going back!). A random shape will have to be done with a different method (I almost had one, but apparently Drawable.getTransparentRegion() returns a null by default, anyone that can help me there let me know), or by defining a rough bounding "box" using this method.


Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. public final class DirectionPad extends Button{
  3.         //Constructors, etc
  5.         @Override
  6.         public boolean onTouchEvent(MotionEvent event){
  7.                 int width = getWidth();
  8.                 int height = getHeight();
  9.                 int CenterX = width/2;
  10.                 int CenterY = height/2;
  11.                 double innerRadius = CenterX/5;
  12.                 double outerRadius = 3*CenterX/5;
  13.                 double pi = Math.PI;
  14.                 int TouchX = (int)event.getX();
  15.                 int TouchY = (int)event.getY();
  16.                 double X = TouchX - CenterX;
  17.                 double Y = -(TouchY - CenterY);
  18.                 double radius = Math.hypot(X,Y);       
  19.                 double theta = Math.atan2(Y,X);
  21.                 if(event.getAction()==MotionEvent.ACTION_UP){
  22.                         if(Y > -X+CenterX || Y < X-CenterX || Y > X + CenterX || Y < -X - CenterX){
  23.                                 return false;
  24.                         }
  25.                         if (radius < innerRadius){
  26.                                 centerClick();
  27.                         }
  28.                         else if(radius < outerRadius){
  29.                                 if(theta > pi/2){
  30.                                         upLeftClick();
  31.                                 }
  32.                                 else if(theta > 0){
  33.                                         upRightClick();
  34.                                 }
  35.                                 else if(theta > -pi/2){
  36.                                         downRightClick();
  37.                                 }
  38.                                 else{
  39.                                         downLeftClick();
  40.                                 }
  41.                         }
  42.                         else {
  43.                                 if(theta<(3*pi/4) && theta > (pi/4)){
  44.                                         upClick();
  45.                                 }
  46.                                 if(theta<(pi/4) && theta > (-pi/4)){
  47.                                         rightClick();
  48.                                 }
  49.                                 if(theta<(-pi/4) && theta > (-3*pi/4)){
  50.                                         downClick();
  51.                                 }
  52.                                 if(theta<(-3*pi/4) || theta > (3*pi/4)){
  53.                                         leftClick();
  54.                                 }
  55.                         }
  56.                 }
  59.                 return super.onTouchEvent(event);
  60.         }
  62. }
Parsed in 0.015 seconds, using GeSHi

What I did here was first to declare a custom class that extended button (I'm a fan of button for no real reason, but I think you can do it with any view) and then override the onTouchEvent(MotionEvent) method. From there, it's time to hit the math.

If you remember from math class, an equation in the form y = f(x) defines a line. Swap out the equal sign for a < or > [y > f(x)] and you get a region. Using a logical AND between two regions and you get the intersection, a logical OR and you get the union. Once you've got that it's pretty easy to define the region you want by logically combining half planes (in rectangular) and/or circles and slices (in polar).

So the first step is to draw the lines you need. For my shape I used the following:

directionpadlayout.png (25.03 KiB) Viewed 1443 times

It consists of two circles and several lines. You should do the outer lines in rectangular, but the circles and inner lines are easier in polar.

Step two is setting up your cartesian coordinate system. Grab the width and the height of the view with getWidth() and getHeight(), also grab the coordinates of the touch event with event.getX() and event.getY(). Because the view's coordinate system starts at the upper left and goes positive to the right and down, we want to change that to a more manageable system where the origin is in the middle and it goes positive to the right and up. Simply subtract the coordinates of the center from their respective touch event coordinate to get the touch location with reference to the center of the view. Flip the sign of the Y expression to account for the fact that the View's Y coordinate goes positive as it goes down instead of up(first time I did this all my regions were inverted top to bottom).

We now have X and Y to work with in rectangular coordinates. To get polar we can use R = sqrt(X^2 + Y^2) and theta = tan^-1(Y/X) or we can cheat and let Java do it for us with R = Math.hypot(X,Y) and theta = atan2(Y,X). Keep in mind that atan2 returns values from -pi to pi, which is to say one counter-clockwise revolution starting at the negative X axis.

We should now have X,Y,R and theta and are good to go!

R = [constant] defines a circle
theta = [constant] = defines a straight line radiating outward from the origin
Y = mX + b defines a straight line of slope m with Y intercept b

Use event.getAction() to get the action, and then I used an if statement to pick the one I wanted (in this case ACTION_UP, the event of finishing a press). Other useful ones are ACTION_DOWN (the start of a press) or ACTION_MOVE (dragging). The first if statement defines the area outside the diamond where no button resides. Returning false will make the area unclickable, and will pass the click to any underlying buttons instead.

A whole bunch of if statements to define my regions later and we can make individual calls for each region. A final call to the superclass's onTouchEvent method rounds things out and it's good to go.

In the main class, instantiate an object of the class, capture it from the xml layout if necessary, and set an onTouchListener on it.

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1.     public class MainClass extends Activity implements OnTouchListener {
  2.     private DirectionPad DirectionPad;
  3.     @Override
  4.     public void onCreate(Bundle savedInstanceState) {
  5.         super.onCreate(savedInstanceState);
  6.         setContentView(R.layout.main);
  8.         DirectionPad = (DirectionPad) findViewById(R.id.direction_pad);
  9.         DirectionPad.setOnTouchListener(this);
  10.        //rest of onCreate
  11.      }
  12. //rest of class
  13. }
Parsed in 0.012 seconds, using GeSHi

Again I myself am also a newbie, so take this all with a grain of salt, but it seems to work. Any more experienced users who see issues with it or ideas for improving it, please feel free to chime in.
Posts: 2
Joined: Fri May 27, 2011 3:49 am


Return to Novice Tutorials

Who is online

Users browsing this forum: No registered users and 5 guests