Just for fun, we're going to provide an example that is similar to the AccelerometerHandler
, but this time we'll give you the compass values along with the pitch and roll of the phone. We'll call the compass value “yaw,” since that's a standard orientation term that nicely defines the value we're seeing.
The only difference between the following code snippet and the previous accelerometer example is the change of the sensor type to TYPE_ORIENTATION and the renaming of the fields from “accel” to “yaw, pitch, and roll.” Otherwise, it works in the same way, and you can easily swap this code into the game as the control handler! Listing 5–6 shows you its code.
package com.badlogic.androidgames.framework.impl;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
public class CompassHandler implements SensorEventListener {
float yaw;
float pitch;
float roll;
public CompassHandler(Context context) {
SensorManager manager = (SensorManager) context
.getSystemService(Context.SENSOR_SERVICE);
if (manager.getSensorList(Sensor.TYPE_ORIENTATION).size() != 0) {
Sensor compass = manager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
manager.registerListener(this, compass,
SensorManager.SENSOR_DELAY_GAME);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// nothing to do here
}
@Override
public void onSensorChanged(SensorEvent event) {
yaw = event.values[0];
pitch = event.values[1];
roll = event.values[2];
}
public float getYaw() {
return yaw;
}
public float getPitch() {
return pitch;
}
public float getRoll() {
return roll;
}
}
What's the worst thing that can happen to us as Android developers? World-stopping garbage collection! If you look at the Input interface definition in Chapter 3, you'll find the getTouchEvents() and getKeyEvents() methods. These methods return TouchEvents and KeyEvents lists. In our keyboard and touch event handlers, we constantly create instances of these two classes and store them in lists that are internal to the handlers. The Android input system fires many of these events when a key is pressed or a finger touches the screen, so we constantly create new instances that are collected by the garbage collector in short intervals. In order to avoid this, we implement a concept known as instance pooling. Instead of repeatedly creating new instances of a class, we simply reuse previously-created instances. The Pool class is a convenient way to implement that behavior. Let's have a look at its code in Listing 5–7.
package com.badlogic.androidgames.framework;
import java.util.ArrayList;
import java.util.List;
public class Pool<T> {
Here are the generics: the first thing to recognize is that this is a generically typed class, much like collection classes such as ArrayList
or HashMap
. Generics allow us to store any type of object in our Pool
without having to cast continuously. So what does the Pool
class do?
public interface PoolObjectFactory<T> {
public T createObject();
}
An interface called PoolObjectFactory is the first thing defined
and is, once again, generic. It has a single method, createObject()
that will return a new object with the generic type of the Pool/PoolObjectFactory
instance.
private final List<T> freeObjects;
private final PoolObjectFactory<T> factory;
private final int maxSize;
The Pool
class has three members. These include an ArrayList
to store pooled objects, a PoolObjectFactory
that is used to generate new instances of the type held by the class, and a member that stores the maximum number of objects the Pool
can hold. The last bit is needed so our Pool
does not grow indefinitely; otherwise, we might run into an out-of-memory exception.
public Pool(PoolObjectFactory<T> factory, int maxSize) {
this.factory = factory;
this.maxSize = maxSize;
this.freeObjects = new ArrayList<T>(maxSize);
}
The constructor of the Pool
class takes a PoolObjectFactory
and the maximum number of objects it should store. We store both parameters in the respective members and instantiate a new ArrayList
with the capacity set to the maximum number of objects.
public T newObject() {
T object = null;
if (freeObjects.size() == 0)
object = factory.createObject();
else
object = freeObjects.remove(freeObjects.size() - 1);
return object;
}
The newObject()
method is responsible for either handing us a brand-new instance of the type held by the Pool
, via the PoolObjectFactory.newObject()
method, or it returns a pooled instance in case there's one in the freeObjectsArrayList
. If we use this method, we get recycled objects as long as the Pool
has some stored in the freeObjects
list. Otherwise, the method creates a new one via the factory.
public void free(T object) {
if (freeObjects.size() < maxSize)
freeObjects.add(object);
}
}
The free()
method lets us reinsert objects that we no longer use. It simply inserts the object into the freeObjects
list if it is not yet filled to capacity. If the list is full, the object is not added, and it is likely to be consumed by the garbage collector the next time it executes.
So, how can we use that class? We'll look at some pseudocode usage of the Pool
class in conjunction with touch events.
PoolObjectFactory<TouchEvent> factory = new PoolObjectFactory<TouchEvent>() {
@Override
public TouchEvent createObject() {
return new TouchEvent();
}
};
Pool<TouchEvent> touchEventPool = new Pool<TouchEvent>(factory, 50);
TouchEvent touchEvent = touchEventPool.newObject();
– do something here –
touchEventPool.free(touchEvent);
First, we define a PoolObjectFactory
that creates TouchEvent
instances. Next we instantiate the Pool
by telling it to use our factory and that it should maximally store 50 TouchEvent
s. When we want a new TouchEvent
from the Pool
, we call the Pool
's newObject()
method. Initially, the Pool
is empty, so it will ask the factory to create a brand new TouchEvent
instance. When we no longer need the TouchEvent
, we reinsert it into the Pool
by calling the Pool
's free()
method. The next time we call the newObject()
method, we get the same TouchEvent
instance and recycle it to avoid problems with the garbage collector. This class is useful in a couple places. Please note that you must be careful to fully reinitialize reused when they're fetched from the Pool
.
The KeyboardHandler
must fulfill a couple tasks. First, it must connect with the View
from which keyboard events are to be received. Next, it must store the current state of each key for polling. It must also keep a list of KeyEvent
instances that we designed in Chapter 3 for event-based input handling. Finally, it must properly synchronize everything since it will receive events on the UI thread while being polled from our main game loop, which is executed on a different thread. This is a lot of work! As a refresher, we'll show you the KeyEvent
class that we defined in Chapter 3 as part of the Input
interface.
public static class KeyEvent {
public static final int KEY_DOWN = 0;
public static final int KEY_UP = 1;
public int type;
public int keyCode;
public char keyChar;
}
This class simply defines two constants that encode the key event type along with three members while holding the type, key code, and Unicode character of the event. With this, we can implement our handler.
Listing 5–8 shows the implementation of the handler with the Android APIs discussed earlier and our new Pool
class.
package com.badlogic.androidgames.framework.impl;
import java.util.ArrayList;
import java.util.List;
import android.view.View;
import android.view.View.OnKeyListener;
import com.badlogic.androidgames.framework.Input.KeyEvent;
import com.badlogic.androidgames.framework.Pool;
import com.badlogic.androidgames.framework.Pool.PoolObjectFactory;
public class KeyboardHandler implements OnKeyListener {
boolean[] pressedKeys = new boolean[128];
Pool<KeyEvent> keyEventPool;
List<KeyEvent> keyEventsBuffer = new ArrayList<KeyEvent>();
List<KeyEvent> keyEvents = new ArrayList<KeyEvent>();
The KeyboardHandler
class implements the OnKeyListener
interface so that it can receive key events from a View
. The members are next.
The first member is an array holding 128 Booleans. We store the current state (pressed or not) of each key in this array. It is indexed by the key's key code. Luckily for us, the android.view.KeyEvent.KEYCODE_XXX
constants (which encode the key codes) are all between 0 and 127, so we can store them in a garbage collector–friendly form. Note that by an unlucky accident, our KeyEvent
class shares its name with the Android KeyEvent
class, of which instances get passed to our OnKeyEventListener.onKeyEvent()
method. This slight confusion is only limited to this handler code. As there's no better name for a key event than “KeyEvent
,” we chose to live with this short-lived confusion.
The next member is a Pool
that holds the instances of our KeyEvent
class. We don't want to make the garbage collector angry, so we recycle all the KeyEvent
objects we create.
The third member stores the KeyEvent
s that have not yet been consumed by our game. Each time we get a new key event on the UI thread, we add it to this list.
The last member stores the KeyEvent
s that we return by calling the KeyboardHandler.getKeyEvents()
. In the following sections, we'll see why we have to double-buffer the key events.
public KeyboardHandler(View view) {
PoolObjectFactory<KeyEvent> factory = new PoolObjectFactory<KeyEvent>() {
@Override
public KeyEvent createObject() {
return new KeyEvent();
}
};
keyEventPool = new Pool<KeyEvent>(factory, 100);
view.setOnKeyListener(this);
view.setFocusableInTouchMode(true);
view.requestFocus();
}
The constructor has a single parameter consisting of the View
from which we want to receive key events. We create the Pool
instance with a proper PoolObjectFactory
, register the handler as an OnKeyListener
with the View
, and finally, make sure that the View
will receive key events by making it the focused View
.
@Override
public boolean onKey(View v, int keyCode, android.view.KeyEvent event) {
if (event.getAction() == android.view.KeyEvent.ACTION_MULTIPLE)
return false;
synchronized (this) {
KeyEvent keyEvent = keyEventPool.newObject();
keyEvent.keyCode = keyCode;
keyEvent.keyChar = (char) event.getUnicodeChar();
if (event.getAction() == android.view.KeyEvent.ACTION_DOWN) {
keyEvent.type = KeyEvent.KEY_DOWN;
if(keyCode > 0 && keyCode < 127)
pressedKeys[keyCode] = true;
}
if (event.getAction() == android.view.KeyEvent.ACTION_UP) {
keyEvent.type = KeyEvent.KEY_UP;
if(keyCode > 0 && keyCode < 127)
pressedKeys[keyCode] = false;
}
keyEventsBuffer.add(keyEvent);
}
return false;
}
Next, we will discuss our implementation of the OnKeyListener.onKey()
interface method, which is called each time the View
receives a new key event. We start by ignoring any (Android) key events that encode a KeyEvent.ACTION_MULTIPLE
event. These are not relevant in our context. This is followed by a synchronized block. Remember, the events are received on the UI thread and read on the main loop thread, so we have to make sure that none of our members are accessed in parallel.
Within the synchronized block, we first fetch a KeyEvent
instance (of our KeyEvent
implementation) from the Pool
. This will either get us a recycled instance or a brand new one, depending on the state of the Pool
. Next, we set the KeyEvent
's keyCode
and keyChar
members based on the contents of the Android KeyEvent
that were passed to the method. Then, we decode the Android KeyEvent type
and set the type of our KeyEvent
, as well as the element in the pressedKey
array, accordingly. Finally, we add our KeyEvent
to the previously defined keyEventBuffer
list.
public boolean isKeyPressed(int keyCode) {
if (keyCode < 0 || keyCode > 127)
return false;
return pressedKeys[keyCode];
}
The next method of our handler the isKeyPressed()
method, which implements the semantics of Input.isKeyPressed()
. First, we pass in an integer that specifies the key code (one of the Android KeyEvent.KEYCODE_XXX
constants) and returns whether that key is pressed or not. We do this by looking up the state of the key in the pressedKey
array after some range checking. Remember, we set the elements of this array in the previous method, which gets called on the UI thread. Since we are working with primitive types again, there's no need for synchronization.
public List<KeyEvent> getKeyEvents() {
synchronized (this) {
int len = keyEvents.size();
for (int i = 0; i < len; i++)
keyEventPool.free(keyEvents.get(i));
keyEvents.clear();
keyEvents.addAll(keyEventsBuffer);
keyEventsBuffer.clear();
return keyEvents;
}
}
}
The last method of our handler is called getKeyEvents()
, and it implements the semantics of the Input.getKeyEvents()
method. Once again, we start with a synchronized block and remember that this method will be called from a different thread.
Next, we loop through the keyEvents
array, and insert all of its KeyEvent
s into our Pool
. Remember, we fetch instances from the Pool
in the onKey()
method on the UI thread. Here, we reinsert them into the Pool
. But isn't the keyEvents
list empty? Yes, but only the first time we invoke that method. To understand why, you have to grasp the rest of the method.
After our mysterious Pool
insertion loop, we clear the keyEvents
list and fill it with the events in our keyEventsBuffer
list. Finally, we clear the keyEventsBuffer
list and return the newly–filled keyEvents
list to the caller. What is happening here?
We'll use a simple example to illustrate this. First, we'll examine what happens to the keyEvents
and the keyEventsBuffer
lists, as well as to our Pool
each time a new event arrives on the UI thread or the game fetches the events in the main thread:
UI thread: onKey() ->
keyEvents = { }, keyEventsBuffer = {KeyEvent1}, pool = { }
Main thread: getKeyEvents() ->
keyEvents = {KeyEvent1}, keyEventsBuffer = { }, pool { }
UI thread: onKey() ->
keyEvents = {KeyEvent1}, keyEventsBuffer = {KeyEvent2}, pool { }
Main thread: getKeyEvents() ->
keyEvents = {KeyEvent2}, keyEventsBuffer = { }, pool = {KeyEvent1}
UI thread: onKey() ->
keyEvents = {KeyEvent2}, keyEventsBuffer = {KeyEvent1}, pool = { }
Pool
yet, so a new KeyEvent
instance (KeyEvent1
) is created and inserted into the keyEventsBuffer
list.getKeyEvents()
on the main thread. getKeyEvents()
takes KeyEvent1
from the keyEventsBuffer
list and puts it into the keyEvents
list that is returns to the caller.Pool
, so a new KeyEvent
instance (KeyEvent2
) is created and inserted into the keyEventsBuffer
list.getKeyEvents()
again. Now, something interesting happens. Upon entry into the method, the keyEvents
list still holds KeyEvent1
. The insertion loop will place that event into our Pool
. It then clears the keyEvents
list and inserts any KeyEvent
into the keyEventsBuffer
, in this case, KeyEvent2
. We just recycled a key event.KeyEvent
in our Pool
, which we happily reuse. Incredibly, there's no garbage collection!This mechanism comes with one caveat, which is that we have to call KeyboardHandler.getKeyEvents()
frequently or the keyEvents
list fills up quickly, and no objects are returned to the Pool
. Problems can be avoided as long as we remember this.
Now it is time to consider fragmentation. In the last chapter, we revealed that multitouch is only supported on Android versions greater than 1.6. All the nice constants we used in our multitouch code (for example, MotionEvent.ACTION_POINTER_ID_MASK
) are not available to us on Android 1.5 or 1.6. We can use them in our code if we set the build target of our project to an Android version that has this API; however, the application will crash on any device running Android 1.5 or 1.6. We want our games to run on all currently available Android versions, so how do we solve this problem?
We employ a simple trick. We write two handlers, one using the single-touch API in Android 1.5, and another using the multitouch API in Android 2.0 and above. This is safe as long as we don't execute the multitouch handler code on an Android device lower than version 2.0. The VM won't load the code, and it won't throw exceptions continuously. All we need to do is find out which Android version the device is running and instantiate the proper handler. You'll see how this works when we discuss the AndroidInput class. For now, let's concentrate on the two handlers.
In order to use our two handler classes interchangeably, we need to define a common interface. Listing 5–9 presents the TouchHandler
interface.
package com.badlogic.androidgames.framework.impl;
import java.util.List;
import android.view.View.OnTouchListener;
import com.badlogic.androidgames.framework.Input.TouchEvent;
public interface TouchHandler extends OnTouchListener {
public boolean isTouchDown(int pointer);
public int getTouchX(int pointer);
public int getTouchY(int pointer);
public List<TouchEvent> getTouchEvents();
}
All TouchHandler
s must implement the OnTouchListener
interface, which is used to register the handler with a View
. The methods of the interface correspond to the respective methods of the Input
interface defined in Chapter 3. The first three are for polling the state of a specific pointer ID, and the last is for getting TouchEvent
s with which to perform event-based input handling. Note that the polling methods take pointer IDs that can be any number and are given by the touch event.
In the case of our single-touch handler, we ignore any IDs other than zero. To recap, we'll recall the TouchEvent
class defined in Chapter 3 as part of the Input
interface.
public static class TouchEvent {
public static final int TOUCH_DOWN = 0;
public static final int TOUCH_UP = 1;
public static final int TOUCH_DRAGGED = 2;
public int type;
public int x, y;
public int pointer;
}
Like the KeyEvent
class, it defines a couple of constants that echo the touch event's type, along with the x- and y-coordinates in the coordinate system of the View
and the pointer ID. Listing 5–10 shows the implementation of the TouchHandler
interface for Android 1.5 and 1.6.
package com.badlogic.androidgames.framework.impl;
import java.util.ArrayList;
import java.util.List;
import android.view.MotionEvent;
import android.view.View;
import com.badlogic.androidgames.framework.Pool;
import com.badlogic.androidgames.framework.Input.TouchEvent;
import com.badlogic.androidgames.framework.Pool.PoolObjectFactory;
public class SingleTouchHandler implements TouchHandler {
boolean isTouched;
int touchX;
int touchY;
Pool<TouchEvent> touchEventPool;
List<TouchEvent> touchEvents = new ArrayList<TouchEvent>();
List<TouchEvent> touchEventsBuffer = new ArrayList<TouchEvent>();
float scaleX;
float scaleY;
We start by letting the class implement the TouchHandler
interface, which also means that we must implement the OnTouchListener
interface. Next, we have three members that store the current state of the touchscreen for one finger, followed by a Pool
and two lists that hold the TouchEvent
s. This is the same as in the KeyboardHandler
. We also have two members, scaleX
and scaleY
. We'll address these in the following sections and use them to cope with different screen resolutions.
NOTE: Of course, we could make this more elegant by deriving the KeyboardHandler and SingleTouchHandler from a base class that handles all matters regarding pooling and synchronization. However, it would have complicated the explanation even more, so instead, we'll write a few more lines of code.
public SingleTouchHandler(View view, float scaleX, float scaleY) {
PoolObjectFactory<TouchEvent> factory = new PoolObjectFactory<TouchEvent>() {
@Override
public TouchEvent createObject() {
return new TouchEvent();
}
};
touchEventPool = new Pool<TouchEvent>(factory, 100);
view.setOnTouchListener(this);
this.scaleX = scaleX;
this.scaleY = scaleY;
}
In the constructor, we register the handler as an OnTouchListener
and set up the Pool
that we use to recycle TouchEvent
s. We also store the scaleX
and scaleY
parameters that are passed to the constructor (ignore them for now).
@Override
public boolean onTouch(View v, MotionEvent event) {
synchronized(this) {
TouchEvent touchEvent = touchEventPool.newObject();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchEvent.type = TouchEvent.TOUCH_DOWN;
isTouched = true;
break;
case MotionEvent.ACTION_MOVE:
touchEvent.type = TouchEvent.TOUCH_DRAGGED;
isTouched = true;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touchEvent.type = TouchEvent.TOUCH_UP;
isTouched = false;
break;
}
touchEvent.x = touchX = (int)(event.getX() * scaleX);
touchEvent.y = touchY = (int)(event.getY() * scaleY);
touchEventsBuffer.add(touchEvent);
return true;
}
}
The onTouch()
method achieves the same outcome as our KeyboardHandler's
onKey()
method; the only difference is that now we handle TouchEvent
s instead of KeyEvent
s. All the synchronization, pooling, and MotionEvent
handling are already known to us. The only interesting thing is that we multiply the reported x- and y-coordinates of a touch event by scaleX
and scaleY
. This is important to remember as we'll return to it in the following sections.
@Override
public boolean isTouchDown(int pointer) {
synchronized(this) {
if(pointer == 0)
return isTouched;
else
return false;
}
}
@Override
public int getTouchX(int pointer) {
synchronized(this) {
return touchX;
}
}
@Override
public int getTouchY(int pointer) {
synchronized(this) {
return touchY;
}
}
The methods isTouchDown()
, getTouchX()
, and getTouchY()
allow us to poll the state of the touchscreen based on the members that we set in the onTouch()
method. The only noticeable thing about them is that they only return useful data for a pointer ID with a value of zero, since this class only supports single-touch screens.
@Override
public List<TouchEvent> getTouchEvents() {
synchronized(this) {
int len = touchEvents.size();
for( int i = 0; i < len; i++ )
touchEventPool.free(touchEvents.get(i));
touchEvents.clear();
touchEvents.addAll(touchEventsBuffer);
touchEventsBuffer.clear();
return touchEvents;
}
}
}
The final method, SingleTouchHandler.getTouchEvents()
, should be familiar to you, and is similar to the KeyboardHandler.getKeyEvents()
methods. Remember that we call this method frequently the touchEvents
list doesn't fill up.
For multitouch handling, we use a class called MultiTouchHandler
, as shown in Listing 5–11.
package com.badlogic.androidgames.framework.impl;
import java.util.ArrayList;
import java.util.List;
import android.view.MotionEvent;
import android.view.View;
import com.badlogic.androidgames.framework.Input.TouchEvent;
import com.badlogic.androidgames.framework.Pool;
import com.badlogic.androidgames.framework.Pool.PoolObjectFactory;
public class MultiTouchHandler implements TouchHandler {
private static final int MAX_TOUCHPOINTS = 10;
boolean[] isTouched = new boolean[MAX_TOUCHPOINTS];
int[] touchX = new int[MAX_TOUCHPOINTS];
int[] touchY = new int[MAX_TOUCHPOINTS];
int[] id = new int[MAX_TOUCHPOINTS];
Pool<TouchEvent> touchEventPool;
List<TouchEvent> touchEvents = new ArrayList<TouchEvent>();
List<TouchEvent> touchEventsBuffer = new ArrayList<TouchEvent>();
float scaleX;
float scaleY;
public MultiTouchHandler(View view, float scaleX, float scaleY) {
PoolObjectFactory<TouchEvent> factory = new PoolObjectFactory<TouchEvent>() {
@Override
public TouchEvent createObject() {
return new TouchEvent();
}
};
touchEventPool = new Pool<TouchEvent>(factory, 100);
view.setOnTouchListener(this);
this.scaleX = scaleX;
this.scaleY = scaleY;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
synchronized (this) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
int pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_ID_MASK)
>> MotionEvent.ACTION_POINTER_ID_SHIFT;
int pointerCount = event.getPointerCount();
TouchEvent touchEvent;
for (int i = 0; i < MAX_TOUCHPOINTS; i++) {
if (i >= pointerCount) {
isTouched[i] = false;
id[i] = -1;
continue;
}
int pointerId = event.getPointerId(i);
if (event.getAction() != MotionEvent.ACTION_MOVE && i != pointerIndex) {
// if it's an up/down/cancel/out event, mask the id to see if we should process it for this touch
// point
continue;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
touchEvent = touchEventPool.newObject();
touchEvent.type = TouchEvent.TOUCH_DOWN;
touchEvent.pointer = pointerId;
touchEvent.x = touchX[i] = (int) (event.getX(i) * scaleX);
touchEvent.y = touchY[i] = (int) (event.getY(i) * scaleY);
isTouched[i] = true;
id[i] = pointerId;
touchEventsBuffer.add(touchEvent);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_CANCEL:
touchEvent = touchEventPool.newObject();
touchEvent.type = TouchEvent.TOUCH_UP;
touchEvent.pointer = pointerId;
touchEvent.x = touchX[i] = (int) (event.getX(i) * scaleX);
touchEvent.y = touchY[i] = (int) (event.getY(i) * scaleY);
isTouched[i] = false;
id[i] = -1;
touchEventsBuffer.add(touchEvent);
break;
case MotionEvent.ACTION_MOVE:
touchEvent = touchEventPool.newObject();
touchEvent.type = TouchEvent.TOUCH_DRAGGED;
touchEvent.pointer = pointerId;
touchEvent.x = touchX[i] = (int) (event.getX(i) * scaleX);
touchEvent.y = touchY[i] = (int) (event.getY(i) * scaleY);
isTouched[i] = true;
id[i] = pointerId;
touchEventsBuffer.add(touchEvent);
break;
}
}
return true;
}
}
@Override
public boolean isTouchDown(int pointer) {
synchronized (this) {
int index = getIndex(pointer);
if (index < 0 || index >= MAX_TOUCHPOINTS)
return false;
else
return isTouched[index];
}
}
@Override
public int getTouchX(int pointer) {
synchronized (this) {
int index = getIndex(pointer);
if (index < 0 || index >= MAX_TOUCHPOINTS)
return 0;
else
return touchX[index];
}
}
@Override
public int getTouchY(int pointer) {
synchronized (this) {
int index = getIndex(pointer);
if (index < 0 || index >= MAX_TOUCHPOINTS)
return 0;
else
return touchY[index];
}
}
@Override
public List<TouchEvent> getTouchEvents() {
synchronized (this) {
int len = touchEvents.size();
for (int i = 0; i < len; i++)
touchEventPool.free(touchEvents.get(i));
touchEvents.clear();
touchEvents.addAll(touchEventsBuffer);
touchEventsBuffer.clear();
return touchEvents;
}
}
// returns the index for a given pointerId or -1 if no index.
private int getIndex(int pointerId) {
for (int i = 0; i < MAX_TOUCHPOINTS; i++) {
if (id[i] == pointerId) {
return i;
}
}
return -1;
}
}
The onTouch()
method looks as intimidating as our test example in the last chapter. However, all we need to do is marry that test code with our event pooling and synchronization, which we've already talked about in detail. The only real difference from the SingleTouchHandler.onTouch()
method is that we handle multiple pointers and set the TouchEvent.pointer
member accordingly (instead of using a value of zero).
The polling methods, isTouchDown()
, getTouchX()
, and getTouchY( )
should look familiar as well. We perform some error–checking and then fetch the corresponding pointer state for the corresponding pointer index from one of the member arrays that we fill in the onTouch()
method.
@Override
public List<TouchEvent> getTouchEvents() {
synchronized (this) {
int len = touchEvents.size();
for (int i = 0; i < len; i++)
touchEventPool.free(touchEvents.get(i));
touchEvents.clear();
touchEvents.addAll(touchEventsBuffer);
touchEventsBuffer.clear();
return touchEvents;
}
}
}
The final method, getTouchEvents()
, is exactly the same as the corresponding method in SingleTouchHandler.getTouchEvents()
. Now that we are equipped with all these handlers, we can implement the Input
interface.
The Input
implementation of our game framework ties together all the handlers we have developed. Any method calls are delegated to the corresponding handler. The only interesting part of this implementation is choosing which TouchHandler
implementation to use, based on the Android version the device is running. Listing 5–12 shows you an implementation called AndroidInput
.
package com.badlogic.androidgames.framework.impl;
import java.util.List;
import android.content.Context;
import android.os.Build.VERSION;
import android.view.View;
import com.badlogic.androidgames.framework.Input;
public class AndroidInput implements Input {
AccelerometerHandler accelHandler;
KeyboardHandler keyHandler;
TouchHandler touchHandler;
We start by letting the class implement the Input
interface defined in Chapter 3. This leads us to three members: an AccelerometerHandler
, a KeyboardHandler
, and a TouchHandler
.
public AndroidInput(Context context, View view, float scaleX, float scaleY) {
accelHandler = new AccelerometerHandler(context);
keyHandler = new KeyboardHandler(view);
if (Integer.parseInt(VERSION.SDK) < 5)
touchHandler = new SingleTouchHandler(view, scaleX, scaleY);
else
touchHandler = new MultiTouchHandler(view, scaleX, scaleY);
}
These members are initialized in the constructor, which takes a Context
, a View
, and the scaleX
and scaleY
parameters, which we can ignore again. The AccelerometerHandler
is instantiated via the Context
parameter, as the KeyboardHandler
needs the View
that is passed in.
To decide which TouchHandler
to use, we simply check the Android version that the application uses to run. This can be done using the VERSION.SDK
string, which is a constant provided by the Android API. It is unclear why this is a string since it directly encodes the SDK version numbers we use in our manifest file. Therefore, we need to make it into an integer in order to do some comparisons. The first Android version to support the multitouch API was version 2.0, which corresponds to SDK version 5. If the current device runs a lower Android version, we instantiate the SingleTouchHandler
; otherwise, we use the MultiTouchHandler
. At an API level, this is all the fragmentation we need to care about. When we start rendering OpenGL, we'll hit a few more fragmentation issues, but there is no need to worry–they are easily resolved, just like the touch API problems.
@Override
public boolean isKeyPressed(int keyCode) {
return keyHandler.isKeyPressed(keyCode);
}
@Override
public boolean isTouchDown(int pointer) {
return touchHandler.isTouchDown(pointer);
}
@Override
public int getTouchX(int pointer) {
return touchHandler.getTouchX(pointer);
}
@Override
public int getTouchY(int pointer) {
return touchHandler.getTouchY(pointer);
}
@Override
public float getAccelX() {
return accelHandler.getAccelX();
}
@Override
public float getAccelY() {
return accelHandler.getAccelY();
}
@Override
public float getAccelZ() {
return accelHandler.getAccelZ();
}
@Override
public List<TouchEvent> getTouchEvents() {
return touchHandler.getTouchEvents();
}
@Override
public List<KeyEvent> getKeyEvents() {
return keyHandler.getKeyEvents();
}
}
The rest of this class is self-explanatory. Each method call is delegated to the appropriate handler, which does the actual work. With this, we have finished the input API of our game framework. Next, we'll discuss graphics.
3.141.33.158