Thursday, January 6, 2011

Cupcake and Donut Compatibility Part 2a (Using newer APIs in backwards compatible applications)

So there's some nice new APIs that you want to incorporate for your Eclair and Gingerbread users.  However, you are required to maintain compatibility with the ever more obsolete Cupcake and Donut versions of Android.  Well, there's a couple ways to accomplish this feat.  In the first section of this part of the series, we will discuss using reflection to access these newer APIs.

To start off with, there are two ways to access the API level at runtime.  They are

Build.VERSION.SDK
and
Build.VERSION.SDK_INT
Build.VERSION.SDK returns a String and is accessible by all versions of Android, while Build.Version.SDK_INT returns an int and can be used by 4+.

The SDK versions of Android are

1 - Android 1.0
2 - Android 1.1
3 - Android 1.5 Cupcake
4 - Android 1.6 Donut
5 - Android 2.0 Eclair
6 - Android 2.0.1 still Eclair
7 - Android 2.1 also still Eclair
8 - Android 2.2 Froyo
9 - Android 2.3 Gingerbread

SDK 1, 2, 5 and 6 are considered obsolete and combined represent less than 0.1% of the user base.

If you are ensuring compatibility with Android 1.5, simply use
Integer.parseInt(Build.VERSION.SDK)
to obtain the value as an integer.

Now, in the example for this tutorial we will make a View subclass and attempt to detect touches with it.  The ability to detect multiple touches simultaneously was introduced in Android 2.0 (SDK 5) so we will be detecting only single point touches when the application is run on Cupcake and Donut and will detect multiple points when run on Eclair and above.

First create your project for using the Cupcake SDK.  Create a new class and extend View and implement OnTouchListener.   It should start off like this:

package com.andylanddev.reflectiontest;

import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

public class CustomView extends View implements OnTouchListener{
public CustomView(Context context) {
super(context);
}

@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
}
The OnTouchListener interface gives us access to the onTouch(View v, MotionEvent e) method.  This will be called anytime the user touches the screen.  The instance of MotionEvent contains all the information we need about how the user touched the screen.

There are three principal methods of MotionEvent in Eclair+ that allow the detection of multiple touches.  The first is getPointerCoint() which simply returns the number of places the user has touched the screen. The second and third are getX(int pointerNumber) and getY(int pointerNumber).  These allow you to get the value of each pointer independently.  In order to access these methods, we will use reflection.  First, declare the instances of the each Method above your header.
private Method getPointerCount, getX, getY; 
 Now, instantiate the methods in your constructor.

try{
    getPointerCount = MotionEvent.class.getMethod("getPointerCount");
    getX = MotionEvent.class.getMethod("getX",Integer.TYPE);
    getY = MotionEvent.class.getMethod("getY",Integer.TYPE);
}catch(NoSuchMethodException e){}
 The class class contains an instance method called getMethod(String methodName, params...).  By calling this on MotionEvent.class we can retrieve the methods without calling them directly.  The params... is for a comma separated list of all the method's parameters' class names.  A NoSuchMethodException will be thrown if these methods do exist (such as in Cupcake and Donut) so we catch the exception, but ignore it and move on.

Now, add the following code to your onTouch method.


String sdk = Build.VERSION.SDK;
//Declare arrays to store coordinate data
ArrayList<String> outputValues = new ArrayList<String>();
//If user is on Eclair or higher
if(Integer.parseInt(sdk) > 4){
    try{
        //Get pointer count
        int pointerCount = (Integer)getPointerCount.invoke(event);
        //Add the values to the arrays 
        for(Integer i = 0; i < pointerCount; i++){
            outputValues.add("Pointer " + i + ": " +
                             (Float)getX.invoke(event, i) + "," + 
                             (Float)getY.invoke(event, i));
        }
    //invoke throws multiple exceptions so for convenience
    //we handle them generically
    }catch(Exception e){}
//If on an Archaic version of Android
}else{
    //Add the one pointer we can get
    xCoord.add(event.getX());
    yCoord.add(event.getY());
}
//Spit out a log of our findings
for(Integer i = 0; i < xCoord.size(); i++){
    Log.e(sdk, outputValues.get(i));
}
The first thing we do is pull the SDK number and create ArrayLists to hold the values we collect.  In the first part of the if, we grab down the coordinates for each pointer.  The invoke(Object, params..) is as if you were calling object.(params...) in the normal way.  The else statement is called for Donut and Cupcake and grabs the single pointer data in the normal fashion.  For simplicity, we just spit out the data in Logcat to see what's going on.

No comments:

Post a Comment