# Dual Analog Stick Controls

I started this question to get people to grasp a little bit more on how to develop analog controls for android devices. But my code is a little outdated so if anyone knows how to implement it better please post your answer, as there are a lots of bugs in the code below as you'll see.

Note: this is only if your app requires a custom built in analog controls. If you have a library that could implement it better then I suggest to use it. Also the analog controls developed here are mostly for use in landscape mode, but it can be altered to support portrait. Also be sure to check if your device supports multitouch (though if it has at least 800Mhz processor in 99% it does).

This is how your controls would optimally look like. If this is for a game then Move1 would be for walking forward and backward, move2 to for strafe left and right. Rotate1 for looking up and down and rotate2 for left and right.

First thing you should do is get your device resolution in on create:

    DisplayMetrics gettotalY = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(gettotalY);
resolutiony= gettotaly.heightPixels;
resolutionx= gettotaly.widthPixels;
help1=(2*resolutiony)/3;
help2=resolutionx/5;
help3=(4*resolutionx)/5;


Help variables only position your screen analog controllers.

This would be your touch listener:

public boolean onTouchEvent(MotionEvent me) {
int pointerCount = me.getPointerCount(),i=0;
int actionCode = me.getAction() & MotionEvent.ACTION_MASK;
while(i<pointerCount)
{
int pointerId = me.getPointerId(i);
i++;
xpos = me.getX(pointerId);
ypos = me.getY(pointerId);
if(xpos<help2 && ypos>help1)
{float alo = resolutionx/10;
float hej = (5*resolutiony)/6;
alo=xpos-alo;
hej=ypos-hej;
hej=-(hej/((5*resolutiony)/6));
alo=alo/(resolutionx/10);
move1=hej;
move2=alo;}
if(xpos>help3 && ypos>help1)
{float alo = (resolutionx*9)/10;
float hej = (5*resolutiony)/6;
alo=xpos-alo;
hej=ypos-hej;
hej=-(hej/((5*resolutiony)/6));
alo=alo/(resolutionx/10);
rotate2=hej;
rotate1=alo;   }

}
if(me.getActionIndex()==0 && actionCode == MotionEvent.ACTION_UP)
{xpos = me.getX(me.getPointerId(0));
ypos = me.getY(me.getPointerId(0));
if(xpos<help2 && ypos>help1)
{move1=0;
move2=0;}
if(xpos>help3 && ypos>help1)
{rotate1=0;
rotate2=0;}

}
if(me.getActionIndex()==1 && actionCode == MotionEvent.ACTION_POINTER_UP)
{xpos = me.getX(me.getPointerId(1));
ypos = me.getY(me.getPointerId(1));
if(xpos<help2 && ypos>help1)
{move1=0;
move2=0;}
if(xpos>help3 && ypos>help1)
{rotate1=0;
rotate2=0;}

}
try {
} catch (Exception e) {
// No need for this...
}

return super.onTouchEvent(me);
}


Since this is a multitouch example there a few things to learn from here. First we do a while loop to see where every finger on the screen is. If you expect to have only one finger at the screen then we probably wouldn't even need to make this loop, but since this is dual analog we need to check for at least two fingers.

Now why would that be at least? Well you know when you are gaming, you put one thumb on the screen and then put the other and then you press the screen with your thumb again and then release the second thumb and by now the array that holds the information about only two fingers on the screen now holds the information about 5 fingers on the screen. This is why we need to check for all the fingers on the screen.

This can get confusing I know, but the more you think about it the more it makes sense to you and you see that the device cannot really tell what you did with your finger when you lifted it up.

Now this listener returns global variables move1,2, rotate1,2 either positive or negative, and from there on you can implement your dual analog controls without any more problems.

However this is a question as this implementation is not flawless as it would seem. Most of the time everything works fine but when you leave the square with your thumb where the analog controller is variables do not return to 0. Does anyone know how to make this code better?

## migrated from stackoverflow.comApr 16 '13 at 12:26

This question came from our site for professional and enthusiast programmers.

• thanks, i did not even know about that site – user1398593 Apr 16 '13 at 11:56
• anyway Thread.sleep in onTouchEvent looks really bad ... maybe you could use some delta time ... – Selvin Apr 16 '13 at 12:25
• i think thread sleep serves it's purpose here quite good – user1398593 Apr 16 '13 at 18:44

### Initial refactoring

Your code was hard to follow, so I applied a few mechanical transformations to clean it up first.

Let's start by renaming a few variables.

void init() {
DisplayMetrics screen = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(screen);

// "resolution" to me sounds like it should be in DPI. Rename!
screenHeight = screen.heightPixels;
screenWidth  = screen.widthPixels;
horizon = (2 * screenHeight) / 3;
left  = (1 * screenWidth) / 5;
right = (4 * screenWidth) / 5;
}


In onTouchEvent(), you have a weaselly i=0, followed two lines later by while (i < pointerCount), followed two lines later by i++. That's a for-loop! Also, why not rename ip as in the example in the documentation?

public boolean onTouchEvent(MotionEvent me) {
int pointerCount = me.getPointerCount();
for (int p = 0; p < pointerCount; p++) {
int pointerId = me.getPointerId(p);

// Shouldn't xpos and ypos be local?
xpos = me.getX(pointerId);
ypos = me.getY(pointerId);

if (xpos < left && ypos > horizon) {          // LHS control
float alo = screenWidth /10;
float hej = (5 * screenHeight) / 6;
alo = xpos - alo;
hej = ypos - hej;
hej = -(hej / ((5 * screenHeight) / 6));
alo = alo / (screenWidth / 10);
move1 = hej;
move2 = alo;
} else if (xpos > right && ypos > horizon) {  // RHS control
float alo = (screenWidth * 9) / 10;
float hej = (5 * screenHeight) / 6;
alo = xpos - alo;
hej = ypos - hej;
hej = -(hej / ((5 * screenHeight) / 6));
alo = alo / (screenWidth / 10);
rotate2 = hej;
rotate1 = alo;
}
}

int actionCode = me.getAction() & MotionEvent.ACTION_MASK;
if (me.getActionIndex() == 0 && actionCode == MotionEvent.ACTION_UP)
{
xpos = me.getX(me.getPointerId(0));
ypos = me.getY(me.getPointerId(0));
if (xpos < left && ypos > horizon) {
move1 = move2 = 0;
} else if (xpos > right && ypos > horizon) {
rotate1 = rotate2 = 0;
}
}
if (me.getActionIndex() == 1 && actionCode == MotionEvent.ACTION_POINTER_UP)
{
xpos = me.getX(me.getPointerId(1));
ypos = me.getY(me.getPointerId(1));
if (xpos < left && ypos > horizon) {
move1 = move2 = 0;
} else if (xpos > right && ypos > horizon) {
rotate1 = rotate2 = 0;
}
}

try {
// FIXME: WHY IS THIS DELAY NEEDED???
} catch (Exception e) {
// No need for this...
}

return super.onTouchEvent(me);
}


### Coordinate system

Let's focus on this snippet:

if (xpos > right && ypos > horizon) {  // RHS control
float alo = (screenWidth * 9) / 10;
float hej = (5 * screenHeight) / 6;
alo = xpos - alo;
hej = ypos - hej;
hej = -(hej / ((5 * screenHeight) / 6));
alo = alo / (screenWidth / 10);
rotate2 = hej;
rotate1 = alo;
}


I've deduced the following diagram of your coordinate system:

There are bugs:

• In your drawing, the controls are circular or square. Your code has rectangular control zones, which are only square if the screen has a 5:3 width:height ratio.
• alo ranges from 0.0 to 1.0 within each control zone which seems reasonable, but hej ranges from 0.0 to 0.2.
• If a pointer is dragged outside the control zone but not released, the event handler keeps updating move1, move2, rotate1, and rotate2 to some possibly crazy values. (alo can have values from -8.0 to +8.0; hej can have values from -0.2 to +1.0.)

My recommendations:

• If you want circular-looking controls, then specify each control zone as a center and a radius.
• If a pointer is dragged outside the control zone not not released, I think that a reasonable behaviour might be to report the orientation, but cap the magnitude of the reading at the control zone radius.

### Pointer lifecycle

There is another bug: If the pointer is released outside the gray rectangles, then that control will continue on autopilot. I suspect that that is not what you intended.

A pointer starts out with an ACTION_DOWN/ACTION_POINTER_DOWN action, followed by ACTION_MOVE actions, and ends with an ACTION_UP/ACTION_POINTER_UP action. You want two modes:

• For down, if the coordinates are within either control zone, note which control that pointer is associated with (using an instance variable).
• For move, update the coordinates of the control associated with the pointer, if it is associated with a control zone.
• For up, reset the readings of the control to 0, if the pointer is associated with a control zone.