Multiple Screen Sizes With Processing for Android

Samsung Galaxy Tab (Android Virtual Device)

In my ongoing effort to port a desktop Processing application to Android, I am now trying to add support for multiple screen sizes. The goal is that the same .apk works on, say, a 3.7″ Milestone equally well as on a 7″ Galaxy Tab and on a multitude of other screens. The Android Dev Guide has a comprehensive page that explains the platform features for supporting different screens. I did my own tests and experiments to better understand the concepts. This article explains my findings and hopefully saves you some time and work.

Note: Android 3.2 introduces “numeric selectors” to manage different screen sizes, which I don’t explain here.

Android abstracts different screens into 4 generalized sizes and 4 generalized densities (as of version 2.3 of the SDK):

Screen sizes: small, normal, large, xlarge

Densities: ldpi (120 dpi), mdpi (160 dpi), hdpi (240 dpi), xhdpi (320 dpi)

Apps can provide layouts and resources for all the 16 different combinations (one set for “normal-hdpi” screens, one set for “large-ldpi” sreens, etc.). If the app does not provide resources for a specific combination, the platform will scale images automatically and employ other tricks to make the app still work. We’ll take a look at this later.

The pixel resolution, as in “800×400″, is not that important to supporting different screens. In fact, the Dev Guide notes that “in Android, applications do not work directly with resolution.” Apps do have access to the actual width and height in pixels of the screen. For normal Android GUI apps, however, the widget classes are going to handle the resolution transparently.

So, what exactly are size and density and how do they correlate to the resolution in pixels?

The density is a function of the screen size and the resolution. A 3.7-inch screen with a resolution of 854×480 pixels has a density of 265 dots per inch (dpi).

Size of a 854×480 screen at 240 dpi

Android collapses all physical densities into the four predefined categories. A density of 265 dpi will be reported as 240 dpi (“hdpi”).

The density tells you how large a bitmap with a specific resolution will appear on the screen. For example, on an “ldpi” screen (120 dpi), a 120×120 bitmap occupies a 1-inch square. (This is only a rough estimate, as Android does not report the exact physical density.)

In your app, you can provide different versions of images for various densities. For example, you can provide a 100×100-pixel bitmap for an “mpdi” screen and a 150×150-pixel bitmap for an “hdpi” screen, which has 1.5 times the density. Both images will appear in roughly the same physical size on the two screens. So, a higher density allows you to provide images that contain more detail.

A higher density does not necessarily mean that you can cram more widgets or more lines of text onto the screen. A 3″ screen is still a 3″ screen, whether it has 160 dpi or 240 dpi. On the “hdpi” screen, you might simply want to scale all images and widgets by a factor of 1.5, so that they are still readable and easily touchable.

To provide density- or size-specific resources, you add a suffix to the sub-folders of your project’s res/ folder. The right resources will automatically be loaded by the Android platform.

res/
    drawable-mdpi/
        icon.png
    drawable-hdpi/
        icon.png
    values/
        strings.xml
    values-large-hdpi/
        strings.xml

If you don’t provide resources for a specific size or density, the platform will load the default resources, which are assumed to be for a “normal-mdpi” screen. If, on an “hdpi” screen, the platform can only find “mdpi” images, it will scale them up for you. This is all transparent to the app.

The Dev Guide describes the rules of loading alternative resources in detail.

FroYo on a 1024×600 160-dpi screen

FroYo on a 1024×600 240-dpi screen

As an example, let’s take a look at two hypothetical 1024×600 screens. One is a 7.4″ screen, so it has a density of 160 dpi. The other one is a 4.9″ screen with a density of 240 dpi. For the first device, the platform uses the 48×48-pixel “mdpi” versions of the app icons, which are .3″ wide on the screen. For the second device, the 72×72-pixel “hdpi” versions are used (exactly 1.5 times as large), which is also .3″ on the screen. (In the screenshots, you can clearly see that the Alarm Clock icon is a different resource.)

Both screens have the same resolution, and the icons appear at the same physical size. But as the first screen uses smaller bitmaps, there is much more empty space between the icons. One could argue that the layout of the second screen looks prettier.

Apparently, that’s also what Samsung found when they made the Galaxy Tab. The Galaxy Tab has a 7″, 1024×600 screen with 170 dpi. Yet, the Tab does not report its density as “mdpi” but as “hdpi”, so the layout looks exactly as in the second screenshot. If they used “mdpi”, the icons would be .28″ wide, with “hdpi”, they are .42″ wide—not a big deal, and I must admit, the layout does look prettier this way.

(Talking about the Galaxy Tab, Samsung offers an add-on for the Android SDK that contains a virtual device skin that you can use for testing your app.)

Okay, how does all this apply to Processing apps?

First and foremost, make sure that your AndroidManifest.xml specifies the correct target SDK. For example, if you are developing for Android 2.2, the target SDK level is 8. As Processing requires at least level 7 to run at all, you’d specify 7 as the minimum SDK level:

<uses-sdk android:minSdkVersion=”7″ android:targetSdkVersion=”8″></uses-sdk>

The same Processing app without the <uses-sdk> element (left) and with targetSdkVersion=

If you don’t have a <uses-sdk> element at all, the platform assumes that your app was written for Android 1.5, for a “normal-mdpi” screen. When your app actually runs on an “hdpi” screen, the platform will scale all loaded images by the factor 1.5, and the width() and height() Processing functions will return the resolution divided by 1.5. For example, on a Milestone with its 854×480 screen, width() and height() would return 569×320. To avoid this, add the <uses-sdk> element.

A Processing app doesn’t use regular widgets or any of the Android layout managers. Therefore, you’ll have to calculate your own layout based on the reported resolution and density. To query the resolution and density, use the DisplayMetrics class:

DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);

For a WVGA854, “normal-hdpi” screen, the values of the DisplayMetrics object are as follows:

DisplayMetrics{density=1.5, width=854, height=480, scaledDensity=1.5, xdpi=240.0, ydpi=240.0}

If your graphics were designed with a medium-density screen in mind, you can scale all graphics by the factor given in the “density” field to match the current screen—if you want to retain the same physical size, that is.

If you need the size classification—”small”, “normal”, “large”, “xlarge”—use the Configuration class:

Configuration config = getResources().getConfiguration();
int size = config.screenLayout & config.SCREENLAYOUT_SIZE_MASK;
if (size == config.SCREENLAYOUT_SIZE_SMALL)
{
    // use special layout for small screens
}

In my Processing app, I store all graphics in the assets/ directory. The screen-specific suffixes do not work there in the same way they do in the res/ directory. Apart from that, for reasons specific to my app, I do not want to access the graphics by the auto-generated constants like “R.drawable.my_image” (if that’s even an option using Processing). Therefore, I just do my own scaling, just like I would for a desktop application where the user can resize the window.

Talking about the auto-generated constants: There is a field “R” in the interface processing.core.PConstants, which conflicts with the namespace for the resource constants. When accessing any Android resources through these constants, you get an error like this:

The primitive type int of R does not have a field string
The primitive type int of R does not have a field drawable
The primitive type int of R does not have a field layout
The primitive type int of R does not have a field id

The solution is to qualify “R” with the package namespace when accessing the resource constants:

String someText = (String) getResources().getText(org.realmike.helloandroid.R.string.someText);

Conclusion

When scaling a desktop application according to the client size of the window, there’s usually no need to worry about the physical size that the UI elements will end up at. It’s all different on a touch screen device, where you want to keep UI elements legible even on small screens, and you want users to interact with the app in a comfortable way. Thinking in terms of densities and screen sizes is a useful abstraction in this scenario.

For multimedia apps (say, games with pre-rendered graphics), there are unique challenges to supporting multiple resolutions and aspect ratios (I haven’t talked about aspect ratios at all). These challenges are not unique to Android, and you can apply the same techniques as to a desktop application.

If Angry Birds looks great on a multitude of devices, screen-independence should be possible for the rest of us to achieve as well.

7 comments to Multiple Screen Sizes With Processing for Android

Leave a Reply

  

  

  

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Ads