Supporting multiple devices on Android

For educational and game apps, there are two concerns for supporting multiple devices on Android. The first is display, and the second is sound. I will discuss both of these below.

Display

Android Developer site talks about supporting multiple displays. The two options are

  1. Use different sized resources for different sized displays in your app – xhdpi, hdpi, mdpi, ldpi, etc. But, then you don’t quite know how big the image will look on the actual device.
  2. Use dp (density independent pixels) to provide sizes for screen elements including images. This will make sure all the images look the same size on all the screens.

I initially went with option 2. However, this meant that the app did not scale automatically for larger devices. What’s the solution?

Auto-scaling

Turns out the solution is similar to what it would be for any other type of layout – use relative layouts with wrap_content. For each content element, provide the size programmatically based on the size and density of the screen.

For our icon image view on the main screen for Kindergarten Shapes, we want each icon to use 150dp X 150dp on a 320dp X 470dp screen. But, on a 640dp X 1024dp screen, we want it to use 300dp X 300dp – there is more room available.

We calculate the scale factor using -

DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
float xr = dm.widthPixels / 320.0f;
float yr = dm.heightPixels / 470.0f;
float scale = Math.min(xr, yr);

We derive from image view and set it’s onMeasure method to return the scaled size -

public class ScaledImageView extends ImageView {

protected final float scale;

public ScaledImageView(Context context, AttributeSet attrs) {
 super(context, attrs);
 scale = PixelUtil.getScale(getContext().getResources().getDisplayMetrics());
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 setMeasuredDimension(Math.round(150*scale), Math.round(150*scale));
 }
}

In the layout, we use wrap_content on our derived image view -


<com.infinut.ScaledImageView
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:adjustViewBounds="true"
 android:scaleType="fitCenter"
 android:src="@drawable/picture"
 android:clickable="true"
 android:id="@+id/match"/>

Now the images scale automatically based on the size and density of the screen available to them.  Larger the screen, bigger they look. Higher the density, larger the image in pixels. We used the same technique for all other views.

Sound

I was not expecting sound to be a problem for multiple devices. But, because of a bug in android - it meant that sounds that should play sequencially were not doing so correctly. Some of the sections were getting cut off whereas others were overlapping, all because android could not correctly determine the length of the sound track for Vorbis ogg files. I tried a partial solution before, but, was not completely happy with it.

The final solution was to hard-code the lengths of the sound tracks, and trigger the next sound in the sequence using a Handler instead of relying on the erroneous OnCompletionListener as suggested in this post.

We wrote a media manager that handles playing multiple sounds using a single Media Player. A Sound object contains the id of the raw ogg file and also it’s duration.

The code for the media manager that handles playing multiple sounds one after the other is -

public class MediaManager {
  static MediaPlayer mediaPlayer = new MediaPlayer();

  Sound[] sounds;
  int current = -1;
  Context context;

  public MediaManager(Context context) {
    this.context = context;
  }
  public void play(Sound[] sounds) {
    this.sounds = sounds;
    this.current = 0; // start from 0
    prepare(mediaPlayer, 0);
    start(mediaPlayer, 0);
  }

  Runnable runnable = new Runnable() {
    public void run() {
      onCompletion(mediaPlayer);
    }
  };
  Handler handler = new Handler();

  public void start(final MediaPlayer mp, final int index) {
    if (index < sounds.length) {
      // setup for next
      handler.postDelayed(runnable, sounds[index].getDuration());
      // play
      mp.start();
    }
  }

  private void prepare(final MediaPlayer mp, final int index) {
    try {
      if (index < sounds.length) {
        mp.reset();
        // prepare
        AssetFileDescriptor afd =
          context.getResources().openRawResourceFd(sounds[index].id);
        mp.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
        mp.prepare();
        afd.close();
     } else {
       mp.reset();
     }
   } catch (IOException ioe) {
     Log.e("Media Player", "Media player exception", ioe);
   }
 }

 public void stop() {
   if (mediaPlayer != null && mediaPlayer.isPlaying()) {
     mediaPlayer.stop();
   }
   handler.removeCallbacks(runnable);
   current = -1;
 }

 public void onCompletion(MediaPlayer mp) {
   if (current >= 0 && current < sounds.length) {
     current++;
     prepare(mp, current);
     start(mp, current);
   }
 }
}

Now sound plays as expected on all devices including my Kindle Fire.

One thought on “Supporting multiple devices on Android

  1. Pingback: Amazon Kindle Fire Sound Delay | Infinut

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s