Android Tricks: Multitouch AND 1.5 Support, Same App

I don’t know about you but I have an emotional attachment to old platforms and APIs. They are often sweet and nice in their primordial simplicity, even with their well-understood limitations. Even when they become obsolete, we developers still remember and miss them.

In case of Android, version 1.5 / API level 3 is technically obsolete but is far from being extinct from the device market. In fact, as of the day I write this article, it is one of the most widely installed Android versions on the market. (And if you add the not-so-different 1.6, they take almost half of the market together.) While subsequent versions brought us a lot of goodies in various areas of the platform API, 80% or more of all functionality our apps need today is present in 1.5.

Sure, there are cases when your app just isn’t meant to run on 1.x. For example, its core functionality is based on the APIs that are absent from API level 3 (e.g. you’re making a live wallpaper), or when you don’t want to support old devices for performance reasons, or due to a marketing decision. However, in many cases what makes developers raise the minSdkVersion bar is the need to use some of the 1.6+ APIs, such as the new contact management facilities, signal strength detection or better animation support.

The good news I’m trying to bring in this article – to those who aren’t yet familiar with this trick – is that you can actually use the new API yet keep minSdkVersion at 3. This trick might not work in all cases, and might look like a hack, but it will work for many situations like this – and who knows, might allow your app to reach out to its grateful, ready-to-pay customers who still use 1.5.

Example problem

Let’s say we’re developing an app that would benefit from supporting “reverse pinch zooming” – that is, zooming something by moving two fingers away from each other on the touch-screen. Everyone knows this cool gesture that was first popularized on iPhone and then spread to other multitouch devices. For example, it could be a photo app or a 3D app or whatever.

Android does support multitouch since API level 5 / version 2.0, and some of the built-in apps support the pinch-zoom gesture. I’m not going to go deep into the subject of gestures in this article. In few words, if we forget about 1.5 support for a moment, we can just use this ScaleGestureDetector class from the built-in 3D Gallery app in our own app. (API level 8 / version 2.2 actually includes this class in the core API, but for now, let’s not complicate the situation further :) ). In order to use it in your custom view, you have to do the following:

  1. Add the class to the source
  2. Instantiate the class by giving it a reference to the Context and to a listener implementation that will handle all captured pinch zoom events
  3. Override onTouchEvent() in the view and pass the captured MotionEvent to the ScaleGestureDetector instance
  4. The detector will magically call your listener when pinch zoom event are detected

If you have an app where you could try this, then do. Pinch zooming really adds some wow-effect to your app’s UI.

However, the big issue is that ScaleGestureDetector won’t work on 1.5. In fact, it won’t even compile if your project is targeted at 1.5. Unfortunately, there is just no way to make it work on 1.5 because API level 3 just does not support multitouch. The MotionEvent version that is there on 1.5 only contains information about one pointer.

Without knowing the right tricks, at this point you would be standing in front of a dilemma – either not supporting pinch zoom for those who have newer devices and expect this kind of gesture, or dropping support for a large part of the target audience who are still at 1.5 or 1.6. (Well, in fact, there is a third option – keeping two separate versions of the app, but that’s such a horrid, pain-in-the-ass idea that I won’t even discuss it.)

The solution

First of all, in Eclipse we set the target API level of the project to 2.0 or 2.1, while setting the minSdkVersion to 3 in the Android manifest. As a result, you will get a warning on the AndroidManifest.xml file. Sorry, but you’ll have to live with it if you want to use this trick.

What we just did was allowing usage of 2.x binary APIs while declaring support for API level 3. From this moment, our app will be accepted as installable on 1.5 devices. However, it’s now up to us to avoid using 2.x API classes that are actually not present on 1.5. But we do use them in ScaleGestureDetector, so let’s do something about that – or the app will break with a low-level linkage or method not found error as soon as it tries to access those 2.x APIs.

Our solution is the following:

  1. Extract an interface from the ScaleGestureDetector class. You can use a standard Eclipse refactoring for that. (You might choose not to include methods that you’re not going to use, into the interface.) As a result, you will now have a new interface – be sure to replace all possible usages of ScaleGestureBuilder with the interface.
  2. Create a dummy implementation of the interface. This will work for 1.5 since it won’t access any 2.x APIs, or in fact do anything else.
  3. Create a factory class which does something like this:
    public abstract class ScaleGestureDetectorFactory {
    	
    	private ScaleGestureDetectorFactory() {}
    	
    	public static ScaleGestureDetectorStub createScaleGestureDetector(Context context,
    																	  OnScaleGestureListener listener) {
    		String className = ScaleGestureDetectorFactory.class.getPackage().getName() + ".";
    		if (Integer.parseInt(Build.VERSION.SDK) >= 5) {
    			className += "ScaleGestureDetector";
    		} else {
    			className += "DummyScaleGestureDetector";
    		}
    		try {
    			Class<?> detectorClass = Class.forName(className);
    			return (ScaleGestureDetectorStub) detectorClass
    						.getConstructor(Context.class, OnScaleGestureListener.class)
    						.newInstance(context, listener);
    		} catch (Exception e) {
    			throw new RuntimeException(e);
    		}
    	}
    }
    

    As you can see, the factory checks the actual API level supported by the device using the standard Build class, and creates an instance of the interface accordingly. Be sure not to include any static references to the 2.x implementation class so it won’t try to load on a 1.x device!

  4. Use the factory to instantiate the correct scale gesture detector whenever you need it.
  5. And, finally, don’t forget to give an alternate way of zooming for 1.x users who won’t be able to pitch-zoom!

This should work like a charm; however, when you try to debug or run the app on an Emulator from Eclipse, it will refuse to run on a 1.5 emulator. That’s because Eclipse knows the target level of your app is 2.x. In order to test your app on an emulator, you have to launch the emulator manually, export the app into an apk and install it using adb: adb install myapp.apk in the command line.

Conclusion

With this simple trick, you can reach out both to 1.5/1.6 and 2.x audiences. It does require some more code, and you pay the price of having a warning in your project, but the benefit will be much bigger in most cases. Have fun!

Tags: , , , , , , , , ,

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>