Evolving with Android APIs

Sometimes you need to use an API that does not appear in all versions of Android. For instance, in the touch example (Chapter 11, Multi-Touch, on page 220), we used some new methods of the MotionEvent class that were not there before Android 2.0. If you try to run that example in the em16 emulator, you will get the error in Figure 13.1.

If you then open the LogCat view in Eclipse (Window > Show View > Other > Android Log) and scroll back a little, you'll find a more detailed error message, which will look something like this:

Could not find method android.view.MotionEvent.getPointerCount, referenced from method org.example.touch.Touch.dumpEvent VFY: unable to resolve virtual method 11:

Landroid/view/MotionEvent;.getPointerCount ()I Verifier rejected class Lorg/example/touch/Touch; Class init failed in newlnstance call (Lorg/example/touch/Touch;) Uncaught handler: thread main exiting due to uncaught exception java.lang.VerifyError: org.example.touch.Touch at java.lang.Class.newInstanceImpl(Native Method) at java.lang.Class.newInstance(Class.java:1472)

The important part is the VerifyError exception. At runtime, Android throws a VerifyError exception whenever it tries to load one class that uses methods that don't exist in a second class. In this case, the Touch class references the getPointerCount( ) method of the MotionEvent class. MotionEvent exists in 1.5, but the getPointerCount() method was not introduced until version 2.0. Note that it didn't complain about the constants we used like ACTION_POINTER_DOWN because they are baked into our program by the compiler at build time.

It would be nice if we could modify the definition of MotionEvent to add the missing method, but unlike JavaScript or ruby, Java doesn't support that. We can do something similar, though, using one of three techniques:

• Subclassing: We could make a new class that extends MotionEvent and adds the new method. Unfortunately, MotionEvent is final, which in Java means it can't be extended, so that won't work here.

• Reflection: Using utilities from the java.lang.reflect package, we could write code to test for the existence of the new method. If it exists, we'd call it, and if doesn't exist, we'd do something else like return 1. That would work, but Java reflection is slow to run and messy to program.

• Delegation and factory: We could create two classes, one that will be used on older versions of Android and one that will be used on newer versions. The first one will implement dummy versions of the new methods, while the second one will delegate all calls to the real new methods. Then we pick which class to create using a static factory method (a method the caller uses instead of the new keyword to create a class). This technique is simple and can be used to handle most API differences, so we'll use it here.

one of our goals should be to minimize the changes to the original Touch class. We start by replacing all references to MotionEvent with a new class, WrapMotionEvent, like this:

Download Touchv2/src/org/example/touch/Touch.java

@Override public boolean onTouch(View v, MotionEvent rawEvent) {

WrapMotionEvent event = WrapMotionEvent.wrap(rawEvent);

private void dumpEvent(WrapMotionEvent event) {

private float spacing(WrapMotionEvent event) { // ...

private void midPoint(PointF point, WrapMotionEvent event) { // ...

In the onTouch() method, we take the raw MotionEvent passed in by Android and convert it to a WrapMotionEvent by calling the static factory method WrapMotionEvent.wrap(). Other than these changes, the rest of the Touch class is...untouched.

Now let's create the WrapMotionEvent class. Delegation is a pretty common thing to do in Java, so Eclipse provides a command to make it easy. Click the org.example.touch package in the Package Explorer view, and then select the File > New > Class command. Enter the new class name (WrapMotionEvent), and press Return. Now add a field inside the class for the event we want to wrap. The code should look like this so far:

Download Touchv2/src/org/example/touch/WrapMotionEvent.java

package org.example.touch;

import android.view.MotionEvent;

public class WrapMotionEvent { protected MotionEvent event;

In the Java editor, click the event variable, and then select the Source > Generate Delegate Methods command from the menu. Eclipse will give you a big list of possible methods, but we don't need the whole list, so deselect all of them and select only the ones that are actually used in the program: getAction(), getPointerCount(), getPointerld(int), getX(), getX(int), getY(), and getY(int). When you are done, the dialog should look like Figure 13.2, on the next page. Click OK to generate the code.

Before doing anything else, save the WrapMotionEvent.java file, and make a copy called EclairMotionEvent.java. We'll come back to that one in a moment.

As it currently stands, WrapMotionEvent calls several methods that don't exist in older Android versions, so we need to replace those. You can tell which ones are a problem by hovering your mouse cursor over each method call.

9 Generate Delegate Methods a I—^-i

Select methods to create delegates for:

□ •

getMetaStateQ

P7] 0

getPointerCountQ

0 e

getPointerld(int)

□ e

getPressureO

□ t

getPressure(int)

□ •

getRawXQ

□ e

getRawYQ

□ e

getSizeQ

getSize(int)

|7) &

getXQ

0 e

getX(int)

□ e

getXPreclsionQ

El «>

getYQ

P7] e

getY(int)

□ ®

getYPrecisionQ

1___i_ ir___i _ rv

Deselect All

Insertion point;

Insertion point;

Select All

Deselect All

After 'event'

! j Generate method comments

The format of the delegate methods may be configured on the Code Templates preference page.

i 7 of 42 selected.

Cancel

Figure 13.2: Let Eclipse create the delegate methods for you.

The new methods will say "Since: API Level 5," and the old methods will say "Since: API Level 1." Another way to tell would be to temporarily change your build target to Android 1.6, rebuild, and see where the errors are.

Here's the new code that will work under Android 1.6:

Download Touchv2/src/org/example/touch/WrapMotionEvent.java

package org.example.touch;

import android.view.MotionEvent;

public class WrapMotionEvent { protected MotionEvent event;

public int getActionC) { return event.getActionC);

public float getX() { return event.getX();

public float getX(int pointerIndex) { verifyPointerlndex(pointerlndex); return getX();

public float getY() { return event.getY();

public float getY(int pointerIndex) { verifyPointerlndex(pointerlndex); return getY();

public int getPointerCount() { return 1;

public int getPointerId(int pointerlndex) { verifyPointerlndex(pointerlndex); return 0;

private void verifyPointerIndex(int pointerlndex) { if (pointerIndex > 0) {

throw new IllegalArgumentException(

"Invalid pointer index for Donut/Cupcake");

In this version, getPointerCount() will always return 1, indicating there is only one finger pressed. That should ensure that getX(int) and getY(int) will never be called with a pointer index greater than zero, but just in case I've added a verifyPointerIndex() method to check for that error.

Next, we need to add the wrap() method and the constructor for the WrapMotionEvent class:

Download Touchv2/src/org/example/touch/WrapMotionEvent.java

protected WrapMotionEvent(MotionEvent event) {

this.event = event;

static public WrapMotionEvent wrap(MotionEvent event) { try {

return new EclairMotionEvent(event); } catch (VerifyError e) {

return new WrapMotionEvent(event);

The wrap( ) method is our static factory method. First it tries to create an instance of the EclairMotionEvent class. That will fail under Android 1.5 and 1.6 because EclairMotionEvent uses the new methods introduced in Android 2.0 (Eclair). If it can't create an EclairMotionEvent class, then it creates an instance of the WrapMotionEvent class instead.3 Now it's time for us to work on the EclairMotionEvent class that we saved earlier. We'll finish it by making it extend WrapMotionEvent and adding a constructor. We can also take out the getAction() method and the zero-parameter versions of getX() and getY() because they exist in all versions of Android and are already implemented in WrapMotionEvent. Here's the full definition:

Download Touchv2/src/org/example/touch/EclairMotionEvent.java

package org.example.touch; import android.view.MotionEvent;

public class EclairMotionEvent extends WrapMotionEvent {

protected EclairMotionEvent(MotionEvent event) { super(event);

public float getX(int pointerlndex) { return event.getX(pointerlndex);

public float getY(int pointerlndex) { return event.getY(pointerlndex);

public int getPointerCount() { return event.getPointerCount();

3. Design pattern purists will probably deride this code, saying I should have created a separate class to hold the factory method and a common interface for both WrapMotion-Event and EclairMotionEvent to implement. But this is simpler, and it works, which is more important in my book.

public int getPointerId(int pointerlndex) { return event.getPointerld(pointerlndex);

If you try to run the program now, it will work in the 1.6 emulator as well as new versions (2.0 and beyond). On old versions, of course, you'll lose some functionality. Multi-touch is not supported on 1.6, so you won't be able to use the pinch zoom gesture to shrink or grow the image. But you will be able to move the image around using the drag gesture.

In a real program, you might have to implement some alternate way to resize the picture when pinch zoom is not available. For example, you could add buttons to zoom in and out.

Just for fun, start up the em15 (1.5 emulator), and run the program there. It looks OK, but try doing the drag gesture. Nothing happens! We've discovered a bug in Android 1.5.

0 0

Post a comment