📜 ⬆️ ⬇️

Maintaining a balance between functionality and compatibility when developing an application

image

Android application developers, who are oriented when developing to all released devices, are surely familiar with this scheme:

image
')
On July 1, 2010, this was the statistics of the running Android versions. With the release of new versions of Android, developers began to think: add new features to the application, provided by the new version, or make it available on as many devices as possible.

Experienced developers have already made sure that these two options are mutually exclusive, and maintaining a balance between them can be painful. In this article I will show you that this is not the case.

A few weeks ago, in our article, we looked at how to handle multitouch on Android 2.0 (Eclair) and above, by getting a simple demo application at the end. In this article we will remake our application so that it works correctly on all versions, up to Android 1.5. You can get the source code of the old application on Google Code .

Problems in the manifest


The uses-sdk tag in AndroidManifest.xml can be specified with two parameters: minSdkVersion and targetSdkVersion . With this you can say that the application is ready to work on older versions, but at the same time it can work on new ones. Now you can use new SDKs for development. But if you use the functionality of the new platform directly, this is what you can see in the system logs:

E/dalvikvm( 792): Could not find method android.view.MotionEvent.getX, referenced from method com.example.android.touchexample.TouchExampleView.onTouchEvent
W/dalvikvm( 792): VFY: unable to resolve virtual method 17: Landroid/view/MotionEvent;.getX (I)F
W/dalvikvm( 792): VFY: rejecting opcode 0x6e at 0x0006
W/dalvikvm( 792): VFY: rejected Lcom/example/android/touchexample/TouchExampleView;.onTouchEvent (Landroid/view/MotionEvent;)Z
W/dalvikvm( 792): Verifier rejected class Lcom/example/android/touchexample/TouchExampleView;
D/AndroidRuntime( 792): Shutting down VM
W/dalvikvm( 792): threadid=3: thread exiting with uncaught exception (group=0x4000fe70)



We incorrectly specified minSdkVersion , and here it is the result. We developed our application on SDK 8 (Froyo), but leaving the parameter minSdkVersion = ”3” (Cupcake), we would say to the application that we are aware of our intentions, and we will not ask for the impossible. If we leave everything as it is - our users with old versions of the SDK will see an ugly error message. Of course, mobs of offended users will rate your application in the Market for 1 star. To avoid this, we must make secure access to the functions of the new platform without angry checks on older versions of the system.

image

Reflection method


Many developers are already familiar with the practice of solving this problem with the help of reflection. Reflection allows the code to interact with the runtime environment and determine when target methods or classes exist, and call them, or create an instance without touching them directly.

The perspective of requests for functions of other platforms, either purposefully or conditionally referring to reflections, is not good. This is ugly. It is slow. This is cumbersome. First of all, its intensive use in code will turn it into garbage that is inconvenient for further support. What if I say that there is a way to write apps for Android 1.5 (Cupcake) using 2.2 (Froyo) using one code and not using reflections?

Delayed loading


Researcher Bill Pugh published and distributed a method of writing singletones in Java with all the advantages of using deferred loading of ClassLoaders. A Wikipedia material explaining this method. The code looks like this:

public class Singleton { <br/>
// Private constructor prevents instantiation from other classes <br/>
private Singleton ( ) { } <br/>
/**<br/>
* SingletonHolder Singleton.getInstance() <br/>
* SingletonHolder.INSTANCE, .<br/>
*/
<br/>
private static class SingletonHolder { <br/>
private static final Singleton INSTANCE = new Singleton ( ) ; <br/>
} <br/>
public static Singleton getInstance ( ) { <br/>
return SingletonHolder. INSTANCE ; <br/>
} <br/>
}


A very important part of his work is described in the comments. Java classes are loaded and initialized at the first access - it creates for the first time instances of a class or gets access to a method or one of its static fields. This is important for us because the classes are checked by the virtual machine only when they are loaded, not before. Now we have everything to write Android applications without using the reflection method.

Compatibility Design


As it turned out, in practice it is quite simple. As a rule, you want your application to degrade correctly on older versions of the platform, removing some functions, or developing alternative ones. Since the Android functionality is only related to the API version, you only have one thing that you should keep track of when designing compatibility.

In most cases, this support option can be implemented as a simple class hierarchy. You can design your application to access version-sensitive functions using a version-independent interface or abstract classes. A subclass in an interface designed to run on new versions of the platform can support new system capabilities, and subclasses designed to run on older versions of the platform should provide application functionality in alternative ways.

Putting principles into practice


At the beginning of this article, I said that we will redo the previously created application for working with multitouch from API version 3 (Cupcake) to version 8 (Froyo). In a previously published article, I noted that GestureDetectors is a useful model for abstracting the processing of sensor events. Then I did not understand how quickly it is implemented and tested. We can remake version-dependent elements of the demo application and implement all of this with the abstract GestureDetector .

Before we get started, we must change our manifest to declare API version 3 support using minSdkVersion in the uses-sdk tag. Keep in mind that we are still focused on version 8, this should also be noted in the targetSdkVersion parameter of your manifest. Now our manifesto will look like this:

<?xml version = "1.0" encoding = "utf-8" ?> <br/>
<manifest xmlns:android = "schemas.android.com/apk/res/android" <br/>
package = "com.example.android.touchexample" <br/>
android:versionCode = "1" <br/>
android:versionName = "1.0" > <br/>
<application android:icon = "@drawable/icon" android:label = "@string/app_name" > <br/>
<activity android:name = ".TouchExampleActivity" <br/>
android:label = "@string/app_name" > <br/>
<intent-filter > <br/>
<action android:name = "android.intent.action.MAIN" /> <br/>
<category android:name = "android.intent.category.LAUNCHER" /> <br/>
</intent-filter > <br/>
</activity > <br/>
</application > <br/>
<uses-sdk android:minSdkVersion = "3" android:targetSdkVersion = "8" /> <br/>
</manifest >


Our TouchExampleView class is not compatible with Android versions up to Froyo due to the use of the ScaleGestureDetector , and is not compatible with versions below Eclair due to the use of the new MotionEvent method that reads data from a multitouch. We must abstract this functionality into classes that will not be loaded on versions that do not support this functionality. To do this, we will create a new class, call it VersionedGestureDetector .

In the example application, the user has 2 gestures available, dragging and dragging (scale). Therefore, VersionedGestureDetector must define two events: onDrag and onScale . TouchExampleView should receive an instance of the VersionedGestureDetector class corresponding to the platform version, filter incoming events through it, and respond accordingly to onDrag and onScale .

The first version of VersionedGestureDetector will be:

public abstract class VersionedGestureDetector { <br/>
OnGestureListener mListener ; <br/>
<br/>
public abstract boolean onTouchEvent ( MotionEvent ev ) ; <br/>
<br/>
public interface OnGestureListener { <br/>
public void onDrag ( float dx, float dy ) ; <br/>
public void onScale ( float scaleFactor ) ; <br/>
} <br/>
}


First, the application starts with the simplest functionality, focused on Cupcake. For simplicity in this example, we will implement support for each version using a static inner class in the VersionedGestureDetector . Of course, you can do this the way you want, while using the delayed load technique shown above, or equivalent. Do not touch classes that directly use functionality that is not supported by this version of the platform.

private static class CupcakeDetector extends VersionedGestureDetector { <br/>
float mLastTouchX ; <br/>
float mLastTouchY ; <br/>
@Override<br/>
public boolean onTouchEvent ( MotionEvent ev ) { <br/>
switch ( ev. getAction ( ) ) { <br/>
case MotionEvent. ACTION_DOWN : { <br/>
mLastTouchX = ev. getX ( ) ; <br/>
mLastTouchY = ev. getY ( ) ; <br/>
break ; <br/>
} <br/>
case MotionEvent. ACTION_MOVE : { <br/>
final float x = ev. getX ( ) ; <br/>
final float y = ev. getY ( ) ; <br/>
mListener. onDrag ( x - mLastTouchX, y - mLastTouchY ) ; <br/>
<br/>
mLastTouchX = x ; <br/>
mLastTouchY = y ; <br/>
break ; <br/>
} <br/>
} <br/>
return true ; <br/>
} <br/>
}


This is a simple implementation of the organization of an onDrag event when the pointer moves around the screen. The values ​​that it takes are equal to the traversed path pointer for X and Y.

Starting with the Eclair version, we must clearly track the pointer identifier to prevent the appearance of additional pointers that go beyond the screen. The basic implementation of onTouchEvent in CupcakeDetector can track pointer movements, but with two tricks. We must add the getActiveX and getActiveY methods to get the corresponding coordinates and redefine them in the EclairDetector to get the correct coordinates of the pointer.
<br/>
private static class CupcakeDetector extends VersionedGestureDetector { <br/>
float mLastTouchX ; <br/>
float mLastTouchY ; <br/>
<br/>
float getActiveX ( MotionEvent ev ) { <br/>
return ev. getX ( ) ; <br/>
} <br/>
float getActiveY ( MotionEvent ev ) { <br/>
return ev. getY ( ) ; <br/>
} <br/>
@Override<br/>
public boolean onTouchEvent ( MotionEvent ev ) { <br/>
switch ( ev. getAction ( ) ) { <br/>
case MotionEvent. ACTION_DOWN : { <br/>
mLastTouchX = getActiveX ( ev ) ; <br/>
mLastTouchY = getActiveY ( ev ) ; <br/>
break ; <br/>
} <br/>
case MotionEvent. ACTION_MOVE : { <br/>
final float x = getActiveX ( ev ) ; <br/>
final float y = getActiveY ( ev ) ; <br/>
mListener. onDrag ( x - mLastTouchX, y - mLastTouchY ) ; <br/>
<br/>
mLastTouchX = x ; <br/>
mLastTouchY = y ; <br/>
break ; <br/>
} <br/>
} <br/>
return true ; <br/>
} <br/>
}


Now EclairDetector , redefined by new getActiveX and getActiveY methods . Most of this code should be familiar to you from the original example described at the beginning of the article.
<br/>
private static class EclairDetector extends CupcakeDetector { <br/>
private static final int INVALID_POINTER_ID = - 1 ; <br/>
private int mActivePointerId = INVALID_POINTER_ID ; <br/>
private int mActivePointerIndex = 0 ; <br/>
@Override<br/>
float getActiveX ( MotionEvent ev ) { <br/>
return ev. getX ( mActivePointerIndex ) ; <br/>
} <br/>
@Override<br/>
float getActiveY ( MotionEvent ev ) { <br/>
return ev. getY ( mActivePointerIndex ) ; <br/>
} <br/>
@Override<br/>
public boolean onTouchEvent ( MotionEvent ev ) { <br/>
final int action = ev. getAction ( ) ; <br/>
switch ( action & MotionEvent. ACTION_MASK ) { <br/>
case MotionEvent. ACTION_DOWN : <br/>
mActivePointerId = ev. getPointerId ( 0 ) ; <br/>
break ; <br/>
case MotionEvent. ACTION_CANCEL : <br/>
case MotionEvent. ACTION_UP : <br/>
mActivePointerId = INVALID_POINTER_ID ; <br/>
break ; <br/>
case MotionEvent. ACTION_POINTER_UP : <br/>
final int pointerIndex = ( ev. getAction ( ) & MotionEvent. ACTION_POINTER_INDEX_MASK ) <br/>
>> MotionEvent. ACTION_POINTER_INDEX_SHIFT ; <br/>
final int pointerId = ev. getPointerId ( pointerIndex ) ; <br/>
if ( pointerId == mActivePointerId ) { <br/>
// This was our active pointer going up. Choose a new <br/>
// active pointer and adjust accordingly. <br/>
final int newPointerIndex = pointerIndex == 0 ? 1 : 0 ; <br/>
mActivePointerId = ev. getPointerId ( newPointerIndex ) ; <br/>
mLastTouchX = ev. getX ( newPointerIndex ) ; <br/>
mLastTouchY = ev. getY ( newPointerIndex ) ; <br/>
} <br/>
break ; <br/>
} <br/>
mActivePointerIndex = ev. findPointerIndex ( mActivePointerId ) ; <br/>
return super. onTouchEvent ( ev ) ; <br/>
} <br/>
}

EclairDetector calls super.onTouchEvent after determining the pointer ID, runs CupcakeDetector to determine the drag event. Multiplatform should not be a reason for code duplication.

Finally, let's add the ScaleGestureDetector , which will implement support for the scaling gesture for Froyo. In order to avoid moving during scaling, we need to add a few changes to the CupcakeDetector . Some touchscreens have scaling issues, so we need to take this into account.
We will add the method shouldDrag to CupcakeDetector which will check before sending the onDrag event.

The final version of CupcakeDetector :

private static class CupcakeDetector extends VersionedGestureDetector { <br/>
float mLastTouchX ; <br/>
float mLastTouchY ; <br/>
<br/>
float getActiveX ( MotionEvent ev ) { <br/>
return ev. getX ( ) ; <br/>
} <br/>
float getActiveY ( MotionEvent ev ) { <br/>
return ev. getY ( ) ; <br/>
} <br/>
<br/>
boolean shouldDrag ( ) { <br/>
return true ; <br/>
} <br/>
@Override<br/>
public boolean onTouchEvent ( MotionEvent ev ) { <br/>
switch ( ev. getAction ( ) ) { <br/>
case MotionEvent. ACTION_DOWN : { <br/>
mLastTouchX = getActiveX ( ev ) ; <br/>
mLastTouchY = getActiveY ( ev ) ; <br/>
break ; <br/>
} <br/>
case MotionEvent. ACTION_MOVE : { <br/>
final float x = getActiveX ( ev ) ; <br/>
final float y = getActiveY ( ev ) ; <br/>
<br/>
if ( shouldDrag ( ) ) { <br/>
mListener. onDrag ( x - mLastTouchX, y - mLastTouchY ) ; <br/>
} <br/>
<br/>
mLastTouchX = x ; <br/>
mLastTouchY = y ; <br/>
break ; <br/>
} <br/>
} <br/>
return true ; <br/>
} <br/>
}


EclairDetector remains unchanged. FroyoDetector below. shouldDrag should return a positive value while inactive scaling.

private static class FroyoDetector extends EclairDetector { <br/>
private ScaleGestureDetector mDetector ; <br/>
public FroyoDetector ( Context context ) { <br/>
mDetector = new ScaleGestureDetector ( context,<br/>
new ScaleGestureDetector. SimpleOnScaleGestureListener ( ) { <br/>
@Override public boolean onScale ( ScaleGestureDetector detector ) { <br/>
mListener. onScale ( detector. getScaleFactor ( ) ) ; <br/>
return true ; <br/>
} <br/>
} ) ; <br/>
} <br/>
@Override<br/>
boolean shouldDrag ( ) { <br/>
return ! mDetector. isInProgress ( ) ; <br/>
} <br/>
@Override<br/>
public boolean onTouchEvent ( MotionEvent ev ) { <br/>
mDetector. onTouchEvent ( ev ) ; <br/>
return super. onTouchEvent ( ev ) ; <br/>
} <br/>
}


Now we have a gesture detector implementation, now we have to find a way to create it. Let's create the VersionedGestureDetector method.

public static VersionedGestureDetector newInstance ( Context context,<br/>
OnGestureListener listener ) { <br/>
final int sdkVersion = Integer. parseInt ( Build. VERSION . SDK ) ; <br/>
VersionedGestureDetector detector = null ; <br/>
if ( sdkVersion < Build. VERSION_CODES . ECLAIR ) { <br/>
detector = new CupcakeDetector ( ) ; <br/>
} else if ( sdkVersion < Build. VERSION_CODES . FROYO ) { <br/>
detector = new EclairDetector ( ) ; <br/>
} else { <br/>
detector = new FroyoDetector ( context ) ; <br/>
} <br/>
detector. mListener = listener ; <br/>
return detector ; <br/>
}


Since we focus on Cupcake, we still do not have access to Build.VERSION.SDK_INT . Instead, we should use the now outdated Build.VERSION.SDK .

Our VersionedGestureDetector is ready, now we need to combine it with TouchExampleView , which has become much shorter.

public class TouchExampleView extends View { <br/>
private Drawable mIcon ; <br/>
private float mPosX ; <br/>
private float mPosY ; <br/>
private VersionedGestureDetector mDetector ; <br/>
private float mScaleFactor = 1. f ; <br/>
public TouchExampleView ( Context context ) { <br/>
this ( context, null, 0 ) ; <br/>
} <br/>
public TouchExampleView ( Context context, AttributeSet attrs ) { <br/>
this ( context, attrs, 0 ) ; <br/>
} <br/>
public TouchExampleView ( Context context, AttributeSet attrs, int defStyle ) { <br/>
super ( context, attrs, defStyle ) ; <br/>
mIcon = context. getResources ( ) . getDrawable ( R. drawable . icon ) ; <br/>
mIcon. setBounds ( 0 , 0 , mIcon. getIntrinsicWidth ( ) , mIcon. getIntrinsicHeight ( ) ) ; <br/>
<br/>
mDetector = VersionedGestureDetector. newInstance ( context, new GestureCallback ( ) ) ; <br/>
} <br/>
@Override<br/>
public boolean onTouchEvent ( MotionEvent ev ) { <br/>
mDetector. onTouchEvent ( ev ) ; <br/>
return true ; <br/>
} <br/>
@Override<br/>
public void onDraw ( Canvas canvas ) { <br/>
super. onDraw ( canvas ) ; <br/>
canvas. save ( ) ; <br/>
canvas. translate ( mPosX, mPosY ) ; <br/>
canvas. scale ( mScaleFactor, mScaleFactor ) ; <br/>
mIcon. draw ( canvas ) ; <br/>
canvas. restore ( ) ; <br/>
} <br/>
private class GestureCallback implements VersionedGestureDetector. OnGestureListener { <br/>
public void onDrag ( float dx, float dy ) { <br/>
mPosX + = dx ; <br/>
mPosY + = dy ; <br/>
invalidate ( ) ; <br/>
} <br/>
public void onScale ( float scaleFactor ) { <br/>
mScaleFactor * = scaleFactor ; <br/>
// Don't let the object get too small or too large. <br/>
mScaleFactor = Math. max ( 0.1f , Math. min ( mScaleFactor, 5.0f ) ) ; <br/>
invalidate ( ) ; <br/>
} <br/>
} <br/>
}


Conclusion


So we adapted our application to work correctly on Android 1.5 through the best new features provided by the platform, and without a single use of reflections. The same principles can be applied to any new Android feature, allowing your application to run on older versions of Android:


To see the final version - visit the “Cupcake” section on Google Code .

Additional Information


In this example, we did not offer an alternative path for the user, using the OS, released before Froyo, since the ScaleGestureDetector became available only in 2.2. For real applications, we would suggest an alternative way. Traditionally, Android phones have hard zoom buttons. The ZoomControls and ZoomButtonsController classes will help you implement this path. Realization of this will be an exercise for the reader.

Source: https://habr.com/ru/post/100081/


All Articles