Posts Tagged 'ui'

Two Progress Bars for Android

I built two different views to show progress bars in Android. One is a horizontal progress bar and the other is a vertical progress bar, made up of a stack of horizontal bars. Here they are displayed in a demo app, running on a Galaxy Nexus phone.

galaxy-nexus-2

galaxy-nexus-1

For the horizontal progress, you can control the colors used, the width and height of the bar, and the number of divisions. For the vertical progress bar, I wanted it to look like a stack of bars. When you run the demo app, you can see the values change by touching a progress bar.

How It Works

The three main parts of this demo app are the following:

  1. The main layout file.
  2. The definition of the custom attributes that are used in the layout file.
  3. The view definition itself: ProgressBarView.

The layout xml file for the activity is activity_main.xml. Within it are three view definitions for the progress bars. The first is the vertical progress bar. Note that it uses a custom view type and note that the view has custom attributes. Examples are bar_initial_value, bar_num_divisions, and bar_orientation. A value of 1 for bar_orientation indicates a vertical progress bar.

<com.wglxy.example.progressbars.ProgressBarView
 custom:bar_initial_value="3"
 custom:bar_num_divisions="5"
 custom:bar_spacing="2dp"
 custom:bar_orientation="1"
 custom:bar_color1="@color/bar_green"
 custom:bar_color2="@color/bar_dark_green"
 android:background="@color/background_black"
 android:padding="20dp"
 android:layout_weight="1"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:layout_margin="32dp"
 />

The first horizontal bar definition looks like this:

<com.wglxy.example.progressbars.ProgressBarView

 custom:bar_initial_value="1"
 custom:bar_num_divisions="4"
 custom:bar_spacing="2dp"
 custom:bar_orientation="0"
 custom:bar_color1="@color/bar_green"
 custom:bar_color2="@color/bar_off"
 android:background="@color/bar_off"
 android:layout_width="64dp"
 android:layout_height="16dp"
 android:layout_margin="20dp"
 />

Inside the activity_main.xml file, there is a line at the top that tells the system that there are custom definitions being used. It is the line with “xmlns:custom”.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:custom="http://schemas.android.com/apk/res/com.wglxy.example.progressbars"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent" 
 >
...
</FrameLayout>

The custom attributes are defined in the values folder in a file named “attrs_views.xml”. Each definition describes the name and format of a custom style that can appear in the layout xml file. The declare-styleable part indicates the name of the view that the attributes apply to.

<resources>
 <declare-styleable name="ProgressBarView">
 <attr name="bar_color1" format="color"/>
 <attr name="bar_color2" format="color"/>
 <attr name="bar_initial_value" format="integer"/>
 <attr name="bar_num_divisions" format="integer"/>
 <attr name="bar_height" format="dimension"/>
 <attr name="bar_spacing" format="dimension"/>
 <attr name="bar_orientation" format="integer"/>
 </declare-styleable>
</resources>

In class definition for ProgressBarView, the attribute values are processed in the constructor code. The code looks to see an attribute is present, and if it is, it overrides the default value that would be used.

public ProgressBarView (Context context, AttributeSet attrs, int style) {
   super (context, attrs, style);
   readAttrs (context, attrs);
 }
/**
 * Read the attribute set and set view variables.
 *
 * @param attrs AttributeSet
 * @return void
 */
private void readAttrs (Context context, AttributeSet attrs) {
  TypedArray a = context.obtainStyledAttributes (attrs, 
                    R.styleable.ProgressBarView);
  final int N = a.getIndexCount();
  for (int i = 0; i < N; ++i) {
     int attr = a.getIndex(i);
     switch (attr) {
     case R.styleable.ProgressBarView_bar_color1:
       int c = a.getColor (attr, Color.WHITE);
       setBarColor1 (c);
       break;
     case R.styleable.ProgressBarView_bar_color2:
       int c2 = a.getColor (attr, Color.GRAY);
       setBarColor2 (c2);
       break;
     case R.styleable.ProgressBarView_bar_initial_value:
       int val = a.getInt (attr, 0);
       setValue (val);
       break;
     case R.styleable.ProgressBarView_bar_num_divisions:
       int numdiv = a.getInt (attr, DEFAULT_NUM_DIVISIONS);
       setNumDivisions (numdiv);
       break;
     case R.styleable.ProgressBarView_bar_height:
       int bh = a.getDimensionPixelSize (attr, DEFAULT_BAR_PIXEL_HEIGHT);
       setBarHeight (bh);
       break;
     case R.styleable.ProgressBarView_bar_spacing:
       int bspace = a.getDimensionPixelSize (attr, DEFAULT_BAR_PIXEL_SPACING);
       setBarSpacing (bspace);
       break;
     case R.styleable.ProgressBarView_bar_orientation:
       int orient = a.getInt (attr, DEFAULT_ORIENTATION);
       setBarOrientation (orient);
       break;
     }
   }  // end for
   a.recycle();
 }

As is typical for a subclass of View, the actual drawing of the view is done in the onDraw method . By the time it runs, all the custom attributes have been handled and their values are in instance variables of the view. My onDraw  method checks to see which orientation is in effect and then calls a method to draw the bars either horizontally or vertically.

@Override protected void onDraw(Canvas canvas) {
 int orient = getBarOrientation ();
if (orient == VERTICAL_ORIENTATION) drawOnCanvasV (canvas);
 else drawOnCanvasH (canvas);
 }

Let’s look at the code for the horizontal progress bar. The progress bar it draws is made up of N rectangles., where N is defined by the number of divisions. For example, if the number of divisions is 10 and current value is 2, 2 bars will be drawn with bar color 1, followed by 8 bars of bar color 2.

It starts by examining the variables that control the display: number of divisions, bar height, spacing, etc. Then it draws N rectangles, one for each of the divisions.

/**
 * Draw progress bar oriented horizontally.
 */
void drawOnCanvasH (Canvas canvas) {
 int maxValue = getNumDivisions ();
 int numValue = getValue ();
 if (numValue > maxValue) numValue = maxValue;
 if (numValue < 0) numValue = 0;
 int bspace = getBarSpacing ();
 int customBarHeight = getBarHeight ();
 int numDivs = getNumDivisions ();
 if (numDivs <= 0) numDivs = 1;
 int currentVal = getValue ();
 int color1 = getBarColor1 ();
 int color2 = getBarColor2 ();
 int vw = getWidth ();
 int vh = getHeight ();
 float vwf = (float) vw;
 float vhf = (float) vh;
// Draw a row of bars. Use up the entire width of the view. 
 // Add spacing between the bars. Number of spaces is 1 less than the numDivs.
 float x = 0.0f + getPaddingLeft ();
 float y = 0.0f + getPaddingTop ();
 float barWidth = (vwf - (float) ((numDivs - 1) * bspace) 
                  - getPaddingLeft () - getPaddingRight ()) 
 / (float) numDivs;
 float deltaX = barWidth + bspace;
 float deltaY = 0.0f;
 float barHeight = vhf - getPaddingTop () - getPaddingBottom ();
 if (customBarHeight > 0) barHeight = customBarHeight;
 int rcolor = color1; 
 // Draw N rectangles. Choose the color based on whether the current value 
 // is larger than the index value for the rectangle.
 // The width of the bar is chosen to account for the space 
 // that gets added between rectangles.
 for (int j = 0; j < numDivs; j++) {
   if (j < numValue) rcolor = color1;
   else rcolor = color2;
   mPaint.setColor (rcolor);
   canvas.drawRect (x, y, x + barWidth, y + barHeight, mPaint);
   x += deltaX;
   y += deltaY;
 }
}

When you use this view in a more dynamic application, you do what you normally do with a view. That is, you set values and call the view’s invalidate method to get the changes to appear. An example of that is shown in the onClick method in the MainActivity class.

public void onClick(View v) {
  ProgressBarView pb = (ProgressBarView) v;
  int numDivisions = pb.getNumDivisions ();
  int val = pb.getValue ();
  val++;
  if (val > numDivisions) val = 0;
  pb.setValue (val);
  pb.invalidate ();
}

Source Code

You can download the source code for this demo from  the Wglxy.com website. Click here: download zip file from wglxy.com. The zip is attached at the bottom of that page. After you import the project into Eclipse, it’s a good idea to use the Project – Clean menu item to rebuild the project.

This demo app was compiled with Android 4.2 (API 17). It works in all API levels from API 10 on up.

References

Creating a View Class 

Multitouch Panning and Zooming Examples for Android

I have done some work to help me understand multitouch features in Android. I built a simple demo with examples of pinch zooming and panning. Most of the examples involve using drawing operations on the canvas of a view.

zoom-pan-blog-01zoom-pan-01

About two years ago, I started some work on moving images in Android. I started with the “Making Sense of Multitouch” blog article and ended up doing some simple things with moving images and then drag-drop based on the Android Launcher (see references for links). It’s interesting to me that I have gone back to that same article for a different purpose. I am working on apps where the basic zoom and pan operations are needed.

Overview of the Examples

1 Basic Multitouch

The first example does the basics of multitouch as explained in the Multitouch article. An image displays on the screen. You can touch the image and move it around. You can also use two fingers to do pinch-zoom and zoom out.

zoom-pan-04

I did this as the first step because I wanted to be sure that I understood the Multitouch article and had a working example.

To understand how this example works, go through the Mulitouch article and then get the source code for my examples (see below). My adaptation of the Multitouch article is in class PanZoomView.

/**
 * This view supports both zooming and panning.
 * What gets shown in the view depends on the onDraw method provided by subclasses.
 * The default onDraw method defined displays R.drawable.zoom_view_sample.
 */
public class PanZoomView extends View

The key things to understand in the article and the code are these:

  • How the canvas moves around. See the call to “canvas.translate(x, y)” in method onDraw.
  • How the canvas is scaled during a pinch-zoom gesture. See the call to :canvas.scale(mScaleFactor, mScaleFactor)” in method onDraw.
  • How onTouchEvent handles touch and multitouch events. It sets variables so the calls to translate and scale work.

2 Canvas

In the second section, there are two examples that do essentially the same thing for handling panning and zooming as the first one. The difference is that instead of an image being moved around and zoomed, it is a circle and a rectangle. Those are drawn on the canvas in the onDraw method of the view.

zoom-pan-05 zoom-pan-06

I did these as the next step in understanding how to build my views for my own app. Zooming an image was step one. Zooming a custom canvas was step 2. To help me understand pinch zoom and the point around which zooming occurs, I display a small circle at the point of focus for the scaling of the view. Place two fingers on the screen and look for that dot, halfway between your two fingers.

The Circles example supports both panning and zooming. The Rectangles example supports only zooming. I found myself getting a bit confused while trying to understand for translation on the canvas and scaling so both examples were useful to me.

3. Pan Zoom Listener

PanZoomListener is an interesting example. It comes from Java Pan / Zoom listener for Android (reference 2 below). Rather than doing a custom view every time you want to have panning and zooming, which is what all the other examples do, you simply set up a listener on a view you already have working.

FrameLayout f = new FrameLayout(context);
FrameLayout.LayoutParams fp = new FrameLayout.LayoutParams (...);
imageView imageView = new ImageView (this);
view.addView (imageView, fp);
view.setOnTouchListener(new PanAndZoomListener(view, imageView, Anchor.TOPLEFT));

That may look like a lot to do, but it’s not nearly as complicated as what you do when you build a view of your own with custom handling of panning and zooming.

It works pretty well, but there are a few things that you’d need to adjust because not all of the operations are completely smooth. I definitely like the idea. I adapted the code and built my own example.

What you see in the example I did is an image. As soon as you touch it, it resizes and centers itself. After that, you can use pinch zoom on it. The focus of the zoom is the point between your two fingers.

zoom-pan-09 zoom-pan-10

4. Image Squares

This example was another step toward getting the right panning and zooming effects in the app that I am working on. In the app, I want to be able to draw a grid of squares on a canvas. In the example, I don’t draw the entire grid but simply some squares along a diagonal from top to bottom. This example supports zooming but not moving the canvas around as you touch the screen with one finger. The focus point of the zoom is shown with a small circle again.

zoom-pan-07

You cannot move the canvas around with a simple touch. However, if you start a pinch-zoom gesture and then move both fingers around the image does shift. I never figured out exactly what was causing that. So watch out for that if build on this example.

5. Fixed Point Zoom

Since I was having a few problems with the canvas jumping around a bit, I decided to try using a fixed point for zooming. The last  example zooms in and out around the center dot shown on the screen.

zoom-pan-11 zoom-pan-14

The code structure of this example is a bit different than the earlier ones. All of the views in these examples are subclasses of a PanZoomView class. The view used in this example is LargeGridView. It defines its own drawOnCanvas method like all the rest do, but it also defines its own onDraw method. By the time I got here, I found that the flexible framework I built in PanZoomView to explore the different aspects of panning and zooming wasn’t quite right for fixed point zooming.

Fixed point zoom is what I have ended up using in a couple of apps.

Conclusion

I have done this article a bit differently than some of my earlier blog tutorials. I have not included as much code and explanations.  I assume that most readers will dig into the code as they need to. Download the source code using the links in the next section.

What I wanted to do here is show the progression of my thinking and understanding as I worked on an interesting problem. If you are new to Android, I suggest following a similar progression. Start simple and add more complex features as your understanding increases.

Source Code

You can download the source code for this demo from  the wglxy.com website. Click here: download zip file from wglxy.com. The zip is attached at the bottom of that page. After you import the project into Eclipse, it’s a good idea to use the Project – Clean menu item to rebuild the project.

This demo app was compiled with Android 4.2 (API 17). It works in all API levels from API 10 on up.

Source code is also available as a zip file from Google Drive.

References

Making Sense of Multitouch – I recommend this to anyone looking for a good starting point for touch handling in Android.

Java / Pan Zoom Listener for Android - Article that describes the PanZoomListener. Definitely, take a look at this one.

Moving Views in Android – Part 1 – a simple example that moves an image when you click a button

Moving Views in Android – Part 2, Drag and Drop – an example based on the Launcher code of Android 2.2.

Three Variations for Image Square Grids in Android

In a new Android app I am working on, I want to display a a square grid of images. I have tried  several variations of doing this using a GridView. Each of the solutions involves using a ViewTreeObserver, which is something I would not have thought of on my own. Fortunately, I found several notes on StackOverflow that pointed me in the right direction.

I’ll describe three of the variations in this blog post. The end result for each variation is good: square images on a grid. My favorite variation is the one in which the sizes of the images are determined dynamically, based on the size of the screen. It requires the least amount of  work and delivers good results.

The other two variations give you more control over the sizes of the image squares. You can choose one size for a 480 x 320 screen, another size for 800 x 480, and still another for a 1024 x 768 tablet. It relies on using resource folders with size qualifiers (examples: values-large, values-xlarge, values-sw600dp, etc.).

The main activity for the application allows you to choose the variation you want.

Variations 1-2

The first variation allows you to specify the size of the images and the containing grid. Its layout definition looks like this.

<FrameLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:background="@color/background"
 >
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/gridView"
 android:color="@color/grid_background"
 android:layout_width="@dimen/image_grid_width"
 android:layout_height="match_parent"
 android:columnWidth="@dimen/image_thumbnail_size"
 android:horizontalSpacing="@dimen/image_thumbnail_spacing"
 android:numColumns="@integer/grid_num_columns"
 android:stretchMode="none"
 android:verticalSpacing="@dimen/image_thumbnail_spacing" >
 </GridView>
</FrameLayout>

Notice that it refers to the following values:

  • image_thumbnail_size
  • image_thumbnail_spacing
  • image_grid_width
  • grid_num_columns

All of those are defined in the values resource folder inside dimens.xml.  To support the different sizes of screens, there are many of these dimens.xml files. Here is one of them. This one would work well for  480 x 320 screen.

<resources>
 <integer name="grid_num_rows">10</integer>
 <integer name="grid_num_columns">10</integer>
 <dimen name="image_thumbnail_size">30dp</dimen>
 <dimen name="image_thumbnail_spacing">1dp</dimen>
 <dimen name="image_grid_width">320dp</dimen>
</resources>

The layout file and the values for the dimensions is only half of the solution. When you use the standard GridView, you have to live with the standard GridView behavior, or find a way to work around it. A standard GridView is designed to fill up the entire width of its parent with however many columns you specify. There are lots of variations of parameters you can try, but I have not found one where you end up with the image height matching the image width.

Thanks to several people who provided answers on StackOverflow and a wonderful example on the Android Developers’ website (see References section below), I learned how to use a ViewTreeObserver to adjust the sizes of the images so the height matches the width being used in the grid. All the details of setting up the GirdView with its ImageAdapter can be seen in the full source code (see below). The most important thing is to understand the following code section that appears in the activity’s onCreate method.

// This listener is used to get the final width of the GridView.
// The column width is used to set the height
// of each view so we get nice square thumbnails.
 mGridView.getViewTreeObserver().addOnGlobalLayoutListener(
 new ViewTreeObserver.OnGlobalLayoutListener() {
 @Override public void onGlobalLayout() {
   if (mAdapter.getNumColumns() == 0) {
      final int numColumns = (int) Math.floor(
         mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing));
      if (numColumns > 0) {
         final int columnWidth =
             (mGridView.getWidth() / numColumns) - mImageThumbSpacing;
         mAdapter.setNumColumns(numColumns);
         mAdapter.setItemHeight(columnWidth);
      }
   }
 }
 });

By the time this code runs, the width of the GridView is known. That allows the width of the images in the grid to be calculated, which is then passed along in the call to the setItemHeight method.

public void setItemHeight(int height) {
  if (height == mItemHeight) {
     return;
   }
   mItemHeight = height;
   mImageViewLayoutParams = new GridView.LayoutParams(mItemHeight, mItemHeight);
   notifyDataSetChanged();
}

This method is defined inside the ImageAdapter that works to fill up the GridView. That explains why it looks so simple. You don’t see code to go through all the image views on the grid to adjust their size. What you see is changing the item height and a redefinition of some LayoutParams. The last line is a call to “notifyDataSetChanged”. That causes the adapter to redo all of its work, which includes all of the calls to its getView method. Inside that is where the image views are set up with the dimensions they should be to be squares. Check that code and you will see where it uses mImageLayoutParams.

I said earlier that there would be many of the dimens.xml files, based on an assumption that you always want the square grid to fill the screen. That might not be the case for all apps. If you wanted to have a grid that was always the same size (e.g. 320 x 320), no matter what size the screen is, you could use a definition like the one above.

To handle the landscape orientation, a second dimens.xml is defined in the values-land folder.

<resources>
 <dimen name="image_thumbnail_size">26dp</dimen>
 <dimen name="image_thumbnail_spacing">1dp</dimen>
 <dimen name="image_grid_width">270dp</dimen>
 </resources>

Note that it uses a slightly smaller grid size because it accounts for the size of the action bar  (or title bar) at the top.

My second variation came about because I wanted to see if I could simplify things by not having to define as many values. The xml for variation 2 does not include the image_grid_width. Because of that the GridView fills the entire width of the screen, which works just fine in portrait orientation, but not so good in landscape.

The images themselves are still squares, but the overall grid is not. I left this variation in this demo app because there might be some situations where expanding to fill the full width is the right thing to do.

Variation 3 – Dynamic Image Sizes

The variation I like the best is the one where the image sizes and the grid size are determined dynamically. Other than specifying the number of columns and the spacing, you do not have to do anything to end up with square images within a square grid.

Its layout file is not as complicated.

<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/frame"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content" 
 android:padding="0dp"
 android:background="@color/debug_background"
 >
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/gridView"
 android:color="@color/grid_background"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_gravity="center" 
 android:gravity="center_horizontal|center_vertical"
 android:columnWidth="@dimen/initial_image_thumbnail_size"
 android:numColumns="@integer/grid_num_columns"
 android:padding="0dp"
 android:stretchMode="none"
 android:horizontalSpacing="@dimen/image_thumbnail_spacing"
 android:verticalSpacing="@dimen/image_thumbnail_spacing" >
</GridView>
</LinearLayout>

This solution builds on the earlier ones. It still depends on there being a ViewTreeObserver. From the other variations, I understood what it could be used for. So I changed it so it would do more that adjust the height of the image views in the grid. Since the grid view height and width are known, it recalculates that view and all the image views contained in it.

mGridView.getViewTreeObserver().addOnGlobalLayoutListener(
 new ViewTreeObserver.OnGlobalLayoutListener() {
 public void onGlobalLayout() {
   // When we get here, the size of the frame view is known.
   // Use those dimensions to set the width of the columns 
   // and the image adapter size.
   if (mAdapter.getNumColumns() == 0) {
     View f = mFrameView;
     int fh = f.getHeight () 
              - f.getPaddingTop () - f.getPaddingBottom ();
     int shortestWidth = fh;
     int fw = f.getWidth () - f.getPaddingLeft () - f.getPaddingRight ();
     if (fw < shortestWidth) shortestWidth = fw;
       int usableWidth = shortestWidth 
                         - (0 + mNumColumns) * mImageThumbSpacing
                         - f.getPaddingLeft () - f.getPaddingRight ();
       usableWidth = shortestWidth - (0 + mNumColumns) * mImageThumbSpacing;
       int columnWidth = usableWidth / mNumColumns;
       mImageThumbSize = columnWidth;
       int gridWidth = shortestWidth;
       // The columnWidth used is an integer. That means that we have a
       // little unused space. Fix that up with padding if the unused 
       // space is more than half of an image.
       int estGridWidth = mNumColumns * columnWidth;
       int unusedSpace = (shortestWidth - estGridWidth);
       boolean addPadding = (unusedSpace * 2) > mImageThumbSize;
       if (addPadding) {
          // This is not a precise calculation. Pad with roughly 1/3 the unused space.
          int pad = unusedSpace / 3;
          if (pad > 0) mGridView.setPadding (pad, pad, pad, pad);
       }
       mGridView.setColumnWidth (columnWidth);
       // Now that we have made all the extra adjustments, resize the grid
       // and have it redo its view one more time.
       LayoutParams lparams = new LinearLayout.LayoutParams (gridWidth, gridWidth);
       mGridView.setLayoutParams (lparams);
       mAdapter.setNumColumns(mNumColumns);
       mAdapter.setItemHeight(mImageThumbSize);
     }
   }
 });

There is a lot going on in the code above. The key points are these:

  • The amount of space available for a GridView is known.
  • The shortest width can be determined and the grid can be sized for that value.
  • With a new shortest width, a new value for column width can be calculated.
  • Forcing the grid to redo all of its image views results in the square images needed.

This variation works everywhere that I have tried it. That includes tablets (like Xoom and Kindle Fire) and large and small screens on phones.

Source Code

You can download the source code form the wglxy.com website. Click here: download sources from wglxy.com. The zip is attached to the bottom of that page. After you import the project into Eclipse, it’s a good idea to use the Project – Clean menu item to rebuild the project.

This demo app was compiled with Android 4.0.3 (API 15). It works in all API levels from API 8 and on.

References

ViewTreeObserver – Using this class with your activity makes it possible to get the information you need to size your images and GridView to have a grid of squares.

How do you retrieve dimensions of a view? – article on Stack Overflow that describes the ViewTreeObserver onGlobalLayout method and how to use it to get the size.

Getting Layout Dimensions in Android- More discussion about getting the width of a view after layout.

Displaying Bitmaps Efficiently – Not only is this a great reference about displaying images, it has the best example I found for adjusting the height of image views in a grid. All the onGlobalLayout handlers I did are based on this example. Once again, we should all thank the writers for the Android Developers’ website.

My Dashboard Interface on Tablets – This is a blog article I did about being able to have an app adapt easily to all the different size screens on devices and tablets. It explains more about the size qualifiers that you can use on values and layout folders.

New Tools For Managing Screen Sizes – a blog article by Dianne Hackborn. It explains how to use the new size qualifiers (e.g. layout-600dp, layout-sw600dp).

Android Images With Clickable Areas – Part 1

I want to display images in Android and have different clickable regions within the image. I have read about two different ways of doing this.  The first is a bit like image maps in HTML. You define areas on the screen, using a list of coordinates, and connect the areas to actions. The second method  involves overlaying a hotspot image on top of the real image being displayed. The overlay image has exactly the same size as the first image. It uses different colored regions  to indicate the hotspots. When the user clicks on the primary image, code runs to check the pixel at the corresponding point in the hotspot image. In this, the first of two articles about image maps in Android, I will explain what I learned for the second technique.

Let’s start with what the app looks like when you touch the screen. Circles appear that indicate roughly where the clickable regions on the screen are. When you touch one of the regions, the image changes.

For example, when you touch the image near the end of the space ship , the app changes to make it look like you started the space ship.

Touching the image a second time, returns you to the initial view of the app, which is the first picture without the orange circles showing.

The way this app is set up with a FrameView and ImageView gives you an easy way to handle scaling. I used a 800 x 480 image for this demo app. It scales automatically to all screen sizes, and it is high enough resolution that it looks good on a tablet. Here’s what it looks like in a portrait orientation.

In a real app, you might disallow portrait orientation because it has a very small usable view. I left it in for testing reasons. I wanted to know that the image and the hotspot image were scaling correctly.

How It Works

Most of what goes on in this app is straightforward stuff in Android: clickable images, changing the drawable of an ImageView, etc. The really interesting part for me was getting the scaling done automatically. Two parts of that were tricky: (1) getting the layout just right so the images would scale correctly on all Android devices and tablets; (2) getting the clickable regions to work and also to scale automatically.

Two images are used in the main layout for this app. They are shown below. The first is the view of the starbase that you have seen above. The second image has several rectangles on it. If you were to overlay the second image on top of the first, you would see that they are close to the stops were the circles were drawn to indicate clickable regions.

The overlay image has exactly the same size as the first image. It uses different colored regions  to indicate the hotspots. When the app is running, the overlay image is used to locate a hotspot, which is a region on the screen that you want to define an onClick handler for. When the user clicks on the primary image, code runs to check the pixel at the corresponding point in the hotspot image. I will explain that a bit later. First let’s look at how the images are laid out on the screen.

The main layout for this app uses a FrameView. It is defined to fill up the entire screen. Within it are two image views. Views defined this way in a FrameView share the space of the frame. The second one appears on top of the first one.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/my_frame"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:background="@color/background" >
<ImageView 
 android:id="@+id/image_areas"
 android:layout_width="fill_parent" 
 android:layout_height="fill_parent" 
 android:scaleType="fitCenter"
 android:visibility="invisible"
 android:src="@drawable/p2_ship_mask" 
 />
 <ImageView
 android:id="@+id/image"
 android:layout_width="fill_parent" 
 android:layout_height="fill_parent" 
 android:scaleType="fitCenter"
 android:src="@drawable/p2_ship_default"
 />

</FrameLayout>

Notice that the image view that shows the masks image is invisible. When the app runs, you do not see it, but the layout manager has included it in the layout. Since both ImageViews use “fill_parent” (same as “match_parent”), they expand to fill the parent view. Scaling is done according to the rules for “fitCenter”, which means centering the image both horizontally and vertically and scaling until either the width or the height matches the parent view’s dimension.

With this layout in place, all that is needed is code in the Activity to handle the screen being touched. The main activity implements View.onTouchListener. The method required by that interface is the onTouch method. Here is that method, shortened a bit to make it easier to explain.

public boolean onTouch (View v, MotionEvent ev) {
 final int action = ev.getAction();
 // (1) 
 final int evX = (int) ev.getX();
 final int evY = (int) ev.getY();
 switch (action) {
 case MotionEvent.ACTION_DOWN :
   if (currentResource == R.drawable.p2_ship_default) {
   nextImage = R.drawable.p2_ship_pressed;
   } 
   break;
 case MotionEvent.ACTION_UP :
   // On the UP, we do the click action.
   // The hidden image (image_areas) has three different hotspots on it.
   // The colors are red, blue, and yellow.
   // Use image_areas to determine which region the user touched.
   // (2)
   int touchColor = getHotspotColor (R.id.image_areas, evX, evY);
   // Compare the touchColor to the expected values. 
   // Switch to a different image, depending on what color was touched.
   // Note that we use a Color Tool object to test whether the 
   // observed color is close enough to the real color to
   // count as a match. We do this because colors on the screen do 
   // not match the map exactly because of scaling and
   // varying pixel density.
   ColorTool ct = new ColorTool ();
   int tolerance = 25;
   nextImage = R.drawable.p2_ship_default;
   // (3)
   if (ct.closeMatch (Color.RED, touchColor, tolerance)) {
      // Do the action associated with the RED region
      nextImage = R.drawable.p2_ship_alien;
   } else {
     //...
   }
   break;
  } // end switch
  if (nextImage > 0) {
    imageView.setImageResource (nextImage);
    imageView.setTag (nextImage);
  }
  return true;
}

There is a lot going on in this method:

(1) The coordinates of the touch point come from the event object. The coordinates are relative to the view.

(2) Given the coordinates of the touch, we look up the color of a pixel at the corresponding point in the mask image (the one with the colored rectangles). The layout rules for a FrameView and identically sized images ensure that the points correspond. The code for that follows:

public int getHotspotColor (int hotspotId, int x, int y) {
  ImageView img = (ImageView) findViewById (hotspotId);
  img.setDrawingCacheEnabled(true); 
  Bitmap hotspots = Bitmap.createBitmap(img.getDrawingCache()); 
  img.setDrawingCacheEnabled(false);
  return hotspots.getPixel(x, y);
}

(3) The color for point is not necessarily the exact color value we used in the hotspot image. The reason for this is that the colors could change a bit as the image is scaled. We use a new ColorTool object to test for a matching color.

public boolean closeMatch (int color1, int color2, int tolerance) {
 if ((int) Math.abs (Color.red (color1) - Color.red (color2)) > tolerance ) 
    return false;
 if ((int) Math.abs (Color.green (color1) - Color.green (color2)) > tolerance ) 
    return false;
 if ((int) Math.abs (Color.blue (color1) - Color.blue (color2)) > tolerance ) 
    return false;
 return true;
} // end match

That’s all the code parts. Let’s summarize what happens as the app runs.

  • The user touches the screen.
  • Because that Activity has an onTouch handler, the method onTouch gets called.
  • The code there looks at the event argument to get the x-y position of the touch.
  • These are coordinates relative to the origin of the ImageView, which is itself embedded in the FrameView.
  • Given the x-y location of the touch, the code locates the hidden hotspot image and finds the pixel at the corresponding location.
  • It then takes the color there and finds the best match for it in the following colors: WHITE, RED, BLUE, YELLOW.
  • It then takes the action defined for that color.

For this demo, I used a simple image editing program: Mac Paintbrush. I took an image and made a copy so I could see where I wanted to add the clickable regions.  I added the three rectangles for the colors first. Then I added white over everything else. I saved that as PNG file.

Source Code

You can download the source code form the wglxy.com website. Click here: download sources from wglxy.com. The zip is attached to the bottom of that page. After you import the project into Eclipse, it’s a good idea to use the Project – Clean menu item to rebuild the project.

This demo app was compiled with Android 2.3.3 (API 10). It works in all API levels after API 8.

Conclusion

This way of adding clickable regions is easy to do and takes full advantage of Android’s handling of different screen sizes, screen densities, and orientations. You do not incur much overhead for the second overlay image. In my case, it was only 4 KB. The original image, as a png, was about 70 KB. For simple transitions from one activity to another, this method works out pretty well.

In my next note, I will take a look at another method of doing image maps and redo this example.

References

  • Overlay to make parts of image clickable – discussion in the Android Developers group. This is where I learned about the overlay image method.
  • ImageMaps for Android – This is a very good example of doing images maps by defining regions in xml files. It displays a US map where you can touch the different states. Something like this will be the subject of my next article on image maps.
  • freepik spaceship – The spaceship image came from the freepik website. It is free for noncommercial use.

Horizontal Scrolling Pages of Images in Android

With the Android V4 compatibility library, you can use Fragments even if you are not using Ice Cream Sandwich (API 14+). That opens up some interesting possibilities for your applications, particularly if you want to be more creative in your use of the extra space available on tablets. For this article, I want to focus on another use for fragments:  horizontal scrolling pages.

I started with an article on the Android Developers’ blog: Horizontal View Swiping With View Pager. It is a very useful article that shows you how to get pages of lists scrolling. I wanted to build a demo app of my own that uses pages. Having spent some time reading the new Android Design website, which places heavy emphasis on visual elements, I decided to figure out how to have pages of images that scroll left and right. My inspiration was the section named “Pictures Are Faster Than Words” on the Design Principles page.

My demo app shows pages of images. It has a structure very similar to the Fragments demo that is in the support package. I started with that and replaced the ListView fragments with GridView fragments. Each GridView holds images. Each of the images has a title. When you long-click on an image, you zoom in on the picture, and from there you can click the picture to see the text for the topic. The figures below show what the app looks like.

Pages of topics scroll left and right, or you can jump to the start or the end of the images by touching the “First” and “Last” buttons. To zoom in on the topic of an image, you use a long-click (long press). I did that because the regular touch events are being used so you can scroll pages. A second touch on the enlarged image takes you to the text associated with the image.

In landscape mode, a different arrangement of images exists. On a small device, it shows one row with three images rather than the 2×2 arrangement in portrait mode.

Figure 5 – Layout of images varies in portrait and landscape

Source Code

The source code for this demo application is available in two places:  (1) download from Wglxy.com. (2) download from Google Docs.  Do a “clean build” in Eclipse if it reports errors immediately after importing the project.

This application has been compiled with API level 10 (2.3.3). It runs on devices and in the emulator from API level 8 to API level 15. It also compiles with 4.03. If you prefer to work at that level, just change the project properties. If you do start your app at the higher level, be sure to do extensive testing at the lower API levels. You want to make sure that you are not using features introduced in API 15 that are not supported in older versions.

How It Works

This demo depends on the ViewPager class in the V4 Compatibility Library. If you download the source code for my demo, you do not have to worry about installation of the support package. It is already in the app libs folder and the project’s build path has already been set correctly. (When you set up your own project, you will want to do those two steps for your project. See the instructions for compatibility library and read Horizontal View Swiping With View Pager. )

The main activity of the demo is the GridViewPager class. It depends on the ViewPager class, which works a lot like ListView and GridView classes. You implement an adapter class that provides the views that it scrolls. The adapter is called by a ViewPager view that you include in the layout for the activity. GridViewPager uses demo_pager.xml for its layout. Inside a FrameLayout and LinearLayout, there is a definition of a ViewPager.

<android.support.v4.view.ViewPager
 android:id="@+id/pager"
 android:layout_width="match_parent"
 android:layout_height="0px"
 android:layout_weight="1">
 </android.support.v4.view.ViewPager>

This definition in the layout file indicates where we want the scrolling pages to appear. For this demo, it is right above the definitions for the First and Last buttons.

Let’s examine the GridViewPager class in greater detail. Because it uses fragments, it is a subclass of FragmentActivity, which is one of the classes provided by the compatibility library.
public class GridViewPager extends FragmentActivity 
implements View.OnLongClickListener

In the onCreate method of GridViewPager, an adapter object is created. The adapter there is a bit like an adapter you create when you use ListView and GridView in Android. Rather than providing views that can appear in a list or in a grid, this adapter provides a set of views that can be scrolled left and right as pages. It uses a layout that embeds the ViewPager view from the compatibility class. As shown below, onCreate starts out by defining its content view using the demo_page.xml file. Then there are a few lines to define a collection of images and topics to be used in the demo. Then there is the creation of an adapter object that does the work of creating each individual page. Note the arguments on the constructor. MyAdapter has access to the entire list of topics and the Resources object so it can divide the images up among the pages.

protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.demo_pager);
 // Create a TopicList for this demo. Save it as the shared instance in TopicList
 Resources res = getResources ();
 String sampleText = res.getString (R.string.sample_topic_text);
 TopicList tlist = new TopicList (sampleText);
 TopicList.setInstance (tlist);
 // Create an adapter object that creates the fragments that we need 
 // to display the images and titles of all the topics.
 mAdapter = new MyAdapter (getSupportFragmentManager(), tlist, res);
 mPager = (ViewPager)findViewById(R.id.pager);
 mPager.setAdapter(mAdapter);

 ...
}

Class MyAdapter is defined as a subclass of FragmentStatePagerAdapter, which in turn is a subclass of FragmentPagerAdapter. That class is designed to work with the ViewPager layout class. Its main responsibility is to provide the page views.

public static class MyAdapter extends FragmentStatePagerAdapter
  private TopicList mTopicList;
  private int mNumItems = 0;
  private int mNumFragments = 0;
public MyAdapter (FragmentManager fm, TopicList db, Resources res) {
  super(fm);
  setup (db, res);
}

Note that there are instance variables for the number of images (items) and the number of page fragments. Those are calculated inside the setup method. They are used inside the getCount and getView methods.

/**
 * Get the number of fragments to be displayed in the ViewPager.
 */
@Override public int getCount() {
 return mNumFragments;
 }
/**
 * Return a new GridFragment that is used to display n items at the position given.
 *
 * @param position int - the position of the fragement; 0..numFragments-1
 */
@Override public Fragment getItem(int position) {
 // Create a new Fragment and supply the fragment number, image position, 
 // and image count as arguments.
 Bundle args = new Bundle();
 args.putInt("num", position+1);
 args.putInt("firstImage", position * mNumItems);
 // Most pages hold mNumItems. The last page might not have the full number of items.
 int imageCount = mNumItems;
 if (position == (mNumFragments-1)) {
    int numTopics = mTopicList.getNumTopics ();
    int rem = numTopics % mNumItems;
    if (rem > 0) imageCount = rem;
 }
 args.putInt("imageCount", imageCount);
 args.putSerializable ("topicList", TopicList.getInstance ());
 // Return a new GridFragment object.
 GridFragment f = new GridFragment ();
 f.setArguments(args);
 return f;
}

You should be able to see the similarity between this kind of adapter and the ones you use for list views and grid views. For this adapter, getCount returns the number of pages (fragments) rather than the number of items in the list. The getItem method returns a fragment object to be displayed as a page.

Let’s look at GridFragment, which is a subclass of Fragment. It is the object that creates a page of images. Each page uses a GridView object. The number of rows and columns is determined by two integer resource values: grid_num_rows and grid_num_cols. The code section below highlights some of the key parts of GridFragment.

public class GridFragment extends Fragment {
... 
@Override public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // Read the arguments and check resource values for number of rows and number of
 // columns so we know how many images to display on this fragment.
 Bundle args = getArguments ();
 mNum = ((args != null) ? args.getInt ("num") : 0);
 ...
}
public void onActivityCreated(Bundle savedInstanceState) {
 super.onActivityCreated(savedInstanceState); 
 // When the activity is created, divide the usable space in the view into columns
 // and put a grid of images in that area.
 Activity a = getActivity ();
 ...
 // Connect the gridview with an adapter that fills up the space.
 gridview.setAdapter (new GridImageAdapter (a, mTopicList, 
                                            mFirstImage, mImageCount, 
                                            cellWidth, cellHeight));
 ...
}
public View onCreateView (LayoutInflater inflater, ViewGroup container, 
                          Bundle savedInstanceState) {
 // Build the view that shows the grid.
 View view = inflater.inflate(R.layout.demo_pager_grid, container, false);
 ...
 return view;
}
} // end class GridFragment

There are few things worth noting in the code above:

  • onCreateView is an important method in any Fragment.
    It gives you the opportunity to define the user interface for the fragment. Here, it inflates the layout defined in demo_pager_grid.xml. That’s where the GridView definition is.
  • A GridImageAdapter is a BaseAdpapter subclass that does a bit more work than other adapter classes you might have built.
    It takes a few more arguments in its constructor. The reason I did this is so I could more precisely control  how many images are displayed in the GridView. For a 2×2 grid, for example, I wanted exactly four images to be displayed, and I wanted all of them to be displayed without having to scroll vertically inside the GridView.
  • GridImageAdapter defines a getItem method that creates the views that show up in the GridView.
    Each of the those views is defined by demo_pager_grid_item.xml. It contains a FrameLayout with an ImageView and TextView inside it.
  • onCreateView is an important method in any Fragment.
    It gives you the opportunity to define the user interface for the fragment. Here, it inflates the layout defined in demo_pager_grid.xml. That’s where the GridView definition is.

Grid Layouts

Upon orientation change, the main view of the application switches from a 2×2 grid to a 3×1 grid. That is controlled by having different resource files in the values folders of the application. The number of rows and columns in the grid come from resource values in the dimens.xml file. Check those two files in the values and values-land folders.

Integers in values/dimens.xml:

<integer name="grid_num_rows">2</integer>
<integer name="grid_num_cols">2</integer>

Integers in values-land/dimens.xml

<integer name="grid_num_rows">1</integer>
<integer name="grid_num_cols">3</integer>

Note that there are additional values folders: values-large, values-large-land, values-sw600dp, values-sw600dp-land. Each of those has a dimens.xml file. Those files are used for large screens and tablets. If you follow this approach, you can build an app where the number of images per page is larger for tablets.

I first used this technique when I was adapting my dashboard user interface to tablets.

Rough Spots

Complexity

I know this demo is bit more complicated than some of the ones I have put together. It has pages of views that are created by an adapter object. Each of the pages has a GridView, and those are also created by an adapter object, but it’s a different kind of adapter object. If you find yourself getting confused by all the different classes, go back to the original example in “Horizontal View Swiping With View Pager“. Be sure you understand paging and fragments well. Then come back to this example.

Managing Bitmaps

A problem I did not address is managing bitmaps. I used relatively small images and not very many of them. I also used the FragmentStatePagerAdapter rather than the other FragmentPagerAdapter. It does something so it holds only the views it needs in memory.

When I get ready to move beyond a demo to a real application, I plan to study “Displaying Bitmaps Efficiently“.

GridView of Images

I wanted to create an app where you could scroll through all the images with horizontal scrolling, rather than the vertical scrolling you can get easily with a GridView. Something that probably needs more work is the way in which I create GridView objects. I came up with a way to use a GridView for each page of images, but I had to go to some extra effort to size the images so there would be no need for vertical scrolling inside the pages.

I am not sure I did that in the easiest manner. I used a DisplayMetrics object and dimension values in dimens.xml.

Activity a = getActivity ();
Resources res = a.getResources ();
DisplayMetrics metrics = new DisplayMetrics();
a.getWindowManager().getDefaultDisplay().getMetrics(metrics);
// From the resource files, determine how many rows 
// and columns are to be displayed.
final int numRows = res.getInteger (R.integer.grid_num_rows);
final int numCols = res.getInteger (R.integer.grid_num_cols);
...
int availableHeight = metrics.heightPixels - heightUsed; 
int availableWidth = metrics.widthPixels - widthUsed;
int cellWidth = availableWidth / numCols;
int cellHeight = availableHeight / numRows;

By starting with metrics.heightPixels and widthPixels, I had to account for all the different elements that took up vertical and horizontal space.

Since starting this example app, I have found a few examples where people have used TableLayout objects for a collection of images. I intend to take a look at that class and see if that might be a better way to lay out the topic images.

(Update: August 31, 2012. I found a good way to set image sizes for a good fit within a grid while doing another demo app. See my post on Image Squares.)

Converting to Android Ice Cream Sandwich

There is a quick way to change this demo so it works on Ice Cream Sandwich. Follow my guide for getting started with ICS. Change the app so it is being compiled by Android 4.03 (or later). Edit the styles.xml file in the values-v14 in the resource folder. The styles file contains the following:

<resources>
  <!-- <style name="Theme.Demo" parent="@android:style/Theme.Holo.Light">
  </style> -->
  <dimen name="title_bar_height">36dp</dimen>
</resources>

Remove the comment markers around the style element. (The comment markers are there so the application builds correctly with Android 2.3.3.)

Basically, for the initial transition to V4, all that you are doing is specifying which of the two Ice Cream Sandwich (ICS) themes you want to use for the title bar. The two choices are Theme.DeviceDefault and Theme.Holo.Light. Running ICS in the emulator, the app looks like this:

Figure 6 – Demo app running on Android ICS

References

Fragments – detailed information about Fragments and how they fit in with Activity objects.

Horizontal View Swiping With View Pager – a great article that explains how to use Fragments for pages that you can scroll horizontally. This was the starting point for my demo app. I modified the example to have pages of images in grid views.

Compatibility Library- information about the Android support package that allows you to use some V4 (Ice Cream Sandwich) features in Android 2.x applications.

Supporting Tablets and Handsets – provides guidance on supporting both worlds: single and multi-pane applications.

Layout Tricks: Merging Layouts – contains an example of how to stack a TextView on top of an ImageView.

New Tools For Managing Screen Sizes – a blog article that explains how to use the new size qualifiers (e.g. values-sw600dp, layout-sw600dp).

Getting Started With Android Ice Cream Sandwich (ICS)

How and when do you make the transition to Android Ice Cream Sandwich (API 14+)? That’s a question I have been asking myself for existing apps and new ones. The relative market share of ICS is still very small — under 4%, as shown in the current report on distribution of devices by Android platforms. Still, I want to be looking ahead so there is less work to do at the time when ICS becomes the Android platform with the largest share.

What I intend to do for existing apps is stick with the themes and styles I had been using for pre-ICS (2.x) devices. For ICS devices, I want to move right away to the new themes provided with Ice Cream Sandwich. The simplest example of how to do this is the HelloWorld app that you get when you create a new Android project in Eclipse. By adding the right style definitions for theme in the layout folders, you can easily get an app with a title bar that is correct for the target platform.  Figure 1 shows the Hello app on a pre-ICS device. Figure 2 shows the app on an ICS device.

  

Figures 1 – 2

I consider this a reasonable first step toward Ice Cream Sandwich. A few other steps are suggested in the last section below.

First Step – Defining the Theme

In order to set up the Hello World app to look right on ICS and pre-ICS devices, all you have to do is set up  theme definitions in two styles.xml files. One goes in the regular res/values folder. The other goes in the special res/values-v14 folder, which indicates that is used only when the API level is 14 or greater.

values
        styles.xml
values-v14
        styles.xml

Inside each of styles.xml files is a definition for the resource named “MyTheme”. When the API level is 14 or higher, the style definition that is used is the one in values-14 styles.xml file. Here is what the two definitions look like:

File values/styles.xml contains:

<resources>
 <style name="MyTheme" parent="@android:style/Theme">
 <!-- Any customizations for your app running on pre-3.0 devices here -->
 </style>
</resources>

File values-v14/styles.xml contains:

<resources>
 <style name="MyTheme" parent="@android:style/Theme.DeviceDefault"></style>
</resources>

The MyTheme style is used in the AndroidManifest to indicate what the default style is for activities in the application.  The key line is the “android:theme” line inside the application definition.

<application
 android:theme="@style/MyTheme"
 android:icon="@drawable/ic_launcher"
 android:label="@string/app_name" >
 <activity
 android:name=".HelloWorldActivity"
 android:label="@string/app_name" >
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 </activity>
 </application>

Note that there are two themes to try in ICS. There is “Theme.Holo.Light” and “Theme.DeviceDefault”. The Holo.Light theme gets you a title bar that has a light background.

All of this is very well explained here: Holo Everywhere (in the section “Using Holo While Supporting 2.x”). The author says, ”Most Android developers will still want to support 2.x devices for a while as updates and new devices continue to roll out.”. That’s an important point.

Source Code

The source code for this demo application is available in two places: (1) download from Google Docs; (2) download from Wglxy.com. The Google Docs zip file is a shared file, which means it should be available for everyone. Still, if  you find that you cannot download it from Google Doc, try the second location.

This application has been compiled with API level 15 (4.0.3). It runs on devices and in the emulator from API level 8 to API level 15. If you start your app this way, be sure to do extensive testing at the lower API levels. You want to make sure that you are not using features introduced in API 15 that are not supported in older versions.

Next Steps

Following this note gets you started with Ice Cream Sandwich (ICS). You will have a Hello World app that you can use as  a starter for other apps. Each of the apps will look like it is an app designed for ICS, at least as far as the title bar is concerned. And for the earlier API levels, it will look just the way you want it there too.

Once you have taken the first step, what should you do next? I don’t have a lot of suggestions to make at this time. What I intend to do is study more of the new features in ICS, including fragments, action bars, etc. I have a found a few resources that might help:

  • Action Bar Compatibility - a compatibility library you can use if you want to build an app that looks the same, no matter whether it is API 8 or API15. The library supports a subset of ICS features.
  • ActionBarSherlock
    If you want the new ICS look and many of the new features of ICS, you should take a look at this package. It makes it so your app looks like an ICS app, even if it is running on an API level before 4.0.

I have to admit that this is a very simple demo app. What is interesting for me is how many times I have had to redo this first step. Between my work projects and my personal projects, I have done this about five times already. Each time I have to start searching the web and the good starting point (the Holo Everywhere note) never seems to make an impression. Whenever I find something where I cannot remember the solution, even a simple one, I know it’s time to write it down and put it in a place where I can find it easily with a Google search.

My Dashboard User Interface On Android Tablets

One of the most popular articles on this blog has been the one on the dashboard user interface pattern: How To Build A Dashboard User Interface In Android. It presents what I hope is an easy to understand explanation of all the different pieces that go into a dashboard on Android. It was done with phones in mind. Recently, I wrote up what I described as a “pretty good solution for tablets“, but I am not very happy with that result. It does not do a good enough job at supporting the larger screens.

  • It fills up only a 400×600 portion of the screen.
  • The image buttons on the main screen and the text labels are just too small.
  • The action bar at the top also looks too small.

It was meant to be a temporary fix, just to get me to tablets quickly. In this note, I present a better solution. It changes the home page so it looks much better on tablets. Here are before and after screenshots for the new dashboard. The old XL is on the left. In the new XL on the right, images and text scale nicely to fill the screen.

Figures 1-2: Portrait orientation for old (left) and new Dashboard XL (click to enlarge)

Figures 3-4: Landscape orientation for old and new Dashboard XL

The end result of this work is a good starting point for a dashboard application that runs on Android phones and on the larger screens of tablets. It makes use of the new features to support larger screens. Full source code for this demo application is provided below.
Here’s what it looks like in the emulator for a few different devices: HTC Evo (480 x 800), Kindle File (600 x 1024), and Motorola Xoom (800 x 1280).
Figure 5
My goal was to have one project that supports all the different versions: one apk file, supporting both handsets and tablets. I think I succeeded.

Some of the things I learned doing this:

  • The new features for supporting larger size screens are very useful. (See New Tools For Managing Screen Sizes.)
  • In addition to the layout folders, the values resource folders can have name qualifiers.  For example: values-xlarge, values-sw600dp.
    I have only one set of images, so the extra values folders are an essential part of the solution.
  • I know more about which SDK I want to use to build my application.
    Currently, I use API 10 (2.3.3) and then check that the code works down to API 8 and up to API 15.
  • In order to do a thorough testing job, you need to know what API level various devices are at.  Kindle Fire, for example, is a 7″ tablet running API 10 (2.3)
  • Even though I did not use them in this example, fragments are going to be useful. The dashboard is still a single pane on tablets.

Goals

I started out with a simple goal, that being to have one project that supports both phones and tablets for all the API levels from API 8 on up. As I was doing this work, I found that there was the possibility that I would need separate sets of image files in the drawable folders. Depending on your application’s complexity, that could add quite a bit to the installed size of your app. So I decided to have a second goal, and that was to minimize the number of separate copies of images.

Overview of My Solution

For this solution, I did the following:

  • Changed the structure of the resource folders to use the new name qualifiers related to screen size.
  • Changed the layout for the home screen layouts to reference size values in a dimensions xml file.
  • Defined a dimens.xml file for each of the different screen sizes I wanted to support.
  • Tested each of the screen sizes in the emulator, in order to be sure that image sizes, text sizes, and image resolution choices were good.

The details of the solution are in the sections below.

I should point out that only the new aspects of the solution are described here. Refer back to the original how-to note on dashboards for the descriptions about the action/title bar, image buttons defined in terms of state list drawables, and style definitions. In fact, it is a good idea to start there. Understand that demo app, and then come back to this one.

Resource Structure

An article on the Android Developers blog got me going in the right direction. In “New Tools For Managing Screen Sizes“, I learned about new size qualifiers that you can use on layout folders. I already knew about -large and -xlarge as name qualifiers. Those were in my first try at supporting tablets. It was the numeric selectors that were new to me. You can have folders of layouts with names like “layout-600dp”, “layout-sw600dp” and “layout-sw720dp”. The “dp” is short for the device-independent measure of pixels, and “sw” is short for “smallest width”. With these, you have a way that you can customize your  layouts for different ranges of screen sizes. For instance, you can do something specific for 600×1024 screens and then change it once screen width hits 720 pixels. The latter one would be what applies to many of the new tablets that are 800×1280.

Numeric selectors for layout folders are a very good thing. However, for me, what was even better was that the same numeric selectors could be used on the values folders too. I wanted to have a single set of images, and it turns out that I could do so, changing the dimensions by having a dimens.xml file for each of the different screen sizes.

In the figure below are a few lines from my dimens.xml to illustrate this point. Notice that there are values related to the dashboard’s home screen images as well as the title bar and text sizes.

<dimen name="title_height">45dip</dimen>
<dimen name="title_bar_text_size">18sp</dimen>
<dimen name="text_body_padding">4sp</dimen>
<dimen name="text_size_small">11sp</dimen>
<dimen name="text_size_medium">14sp</dimen>
<dimen name="text_size_large">22sp</dimen>
<dimen name="home_button_text_size">20sp</dimen>
<dimen name="home_button_size">120dp</dimen>
<dimen name="home_button_size_x">120dp</dimen>
<dimen name="home_button_size_y">80dp</dimen>

I then made sure that all of the layout xml files made use of these values. Since there is a dimens.xml file in each of the size-specific values folders, you have what you need to fine-tune how the app looks on various size screens. Compare the values in the default dimens.xml file (above) to the one used for screens classified as “xlarge” (below).

<dimen name="title_height">60dip</dimen>
<dimen name="title_bar_text_size">24sp</dimen>
<dimen name="text_body_padding">4sp</dimen>
<dimen name="text_size_small">14sp</dimen>
<dimen name="text_size_medium">18sp</dimen>
<dimen name="text_size_large">24sp</dimen>
<dimen name="home_button_text_size">28sp</dimen>
<dimen name="home_button_size">216dp</dimen>
<dimen name="home_button_size_x">216dp</dimen>
<dimen name="home_button_size_y">144dp</dimen>

Strategy for Images

I have one set of images in the drawable folder. All the different configurations get them from there so they are reasonably high resolution images. Early on, I experimented with having a different set of images. Once I learned that values folders allowed name qualifiers, I found that I could adjust the scale of images in my dimens.xml and the single set approach worked fine.

If you like, you can always add additional drawables later. But start out with images that look good on the largest screen you plan to support. Then edit the dimens.xml to scale appropriately for the smaller screens.

I have one more thing to note about the images. I only took the time to generate one high res image: the light bulb image. The others are a bit fuzzy on large screens. Still, I have shown that this approach works.

Images on the Home Screen

My earlier Dashboard demo apps used a very simple layout. The old layout for the first two buttons is shown below. Notice that there are no size values. It’s easy to understand and edit.

Figure 6 – Old layout for first two home images

In order to get the easy scaling of images and text, I had to make the layout a little more complicated and do part of the setup work in code. The new layout makes use of FrameLayouts that each get an image button added to them in onCreate of the HomeActivity class.

Figure 7 – New layout for first two home images

The code in onCreate creates a view for each button by inflating the layout shown in Figure 8 below. Note that the layout uses size values defined in the dimens.xml files. As with the prior layout, each of the buttons have onClickFeature as their onClick method.

Figure 8 – Layout used to inflate an image button

The code in onCreate adds the buttons that appear on the home screen. Buttons are created with a LayoutInflater. When the buttons are created, the values from the appropriate dimens.xml file are used. Comments in the code below mark the places where the layout is inflated, the label is assigned to the image button, and where the inflated view is attached to the FrameLayout defined in HomeActivity layout.

LayoutInflater li = this.getLayoutInflater();
int imageButtonLayoutId = R.layout.activity_home_button;
for (int j = 0; j < NUM_HOME_BUTTONS; j++) {
  int frameId = mFrameIds [j];
  int labelId = mLabelIds [j];
  int imageId = mImageIds [j];
  // Inflate a view for the image button. Set its image and label.
  View v = li.inflate (imageButtonLayoutId, null);
  ImageView iv = (ImageView) v.findViewById (R.id.home_btn_image);
  if (iv != null) {
     iv.setImageResource (imageId);
     // Assign a value for the tag so the onClickFeature
     // handler can determine which button was clicked.
     iv.setTag (new Integer (j+1));
  }
  // Set the label
  TextView tv = (TextView) v.findViewById (R.id.home_btn_label);
  if (tv != null) tv.setText (labelId);
  // Find the frame where the image goes.
  // Attach the inflated view to that frame.
  View buttonView = v;
  FrameLayout frame = (FrameLayout) findViewById (frameId);
  if (frame != null) {
     FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams
             (ViewGroup.LayoutParams.MATCH_PARENT,
              ViewGroup.LayoutParams.MATCH_PARENT,
              Gravity.CENTER);
     frame.addView (buttonView, lp);
  }

} // end for 

Three arrays at the start of HomeActivity hold the resource ids for the images and labels that appear on the screen and the resource id numbers of the frames.

// Image resources for the buttons
private Integer[] mImageIds = {
 R.drawable.home_button1, R.drawable.home_button2,
 R.drawable.home_button3, R.drawable.home_button4,
 R.drawable.home_button5, R.drawable.home_button6 } ;

// Labels for the buttons
private Integer[] mLabelIds = {
 R.string.title_feature1, R.string.title_feature2,
 R.string.title_feature3, R.string.title_feature4,
 R.string.title_feature5, R.string.title_feature6 } ;

// Ids for the frames that define where the images go
private Integer[] mFrameIds = {
 R.id.frame1, R.id.frame2,
 R.id.frame3, R.id.frame4,
 R.id.frame5, R.id.frame6 } ;

Overall this solution is a bit more work and not as easy to understand as the old solution in the original dashboard article. Considering that the same code and layouts support both phones and tablets, I think it is worth the added complexity.

Other Considerations

As I was searching for a solution, I tried a few other approaches. I tried using a GridView to lay out the home screen. That almost worked out, but it did not apply the size values in the image button layout the way I wanted. There was also a problem with getting it to use the entire home screen area. It tended to fill from the top and leave extra space at the bottom. If you were inclined to using a GridView, you might be able to fix those problems.

I found the Kindle Fire to be an interesting test case. It is 600×1024 and classified by Android as an “xlarge” screen. It runs at API level 10. That means that the new size qualifiers (sw-600dp, sw-720dp, etc.) do not apply. Fortunately, as pointed in the reference articles (see below), a hybrid solution is supported. You can have resource folders for the older levels and the newer levels in your packaged app. For the Kindle Fire, the “values-xlarge” and “layout-xlarge” folders are used because that is what a API 10 running device knows about. That same API level is unaware of the newer folders. There is a suggestion in the “new tools” reference that you should plan ahead, meaning that one day Kindle Fire will move up to a higher API level and you should put in place whatever support you need ahead of time so your app upgrades itself when they make the change. So that means filling in the sw-600dp xml files, which is what I have done in an app I have already released.

A tablet like the Motorola Xoom is running Honeycomb (3.2), so it looks for the new resource folders. The definitions in values-sw720dp and layout-sw720dp apply. Of course, if you wanted, you could have 800dp folders for its 800×1280 screen. The beauty of the new tools in Android is that you decide what works for your application and how many different screen sizes you want to address with custom layouts.

Apparently the Galaxy Tablet, the API 10 level version, is also a special case. The references indicate that it qualifies as “large” screen. I used it as representative tablet for testing in that size range.

I did not say much about text size, besides noting that there are text size values in the dimens.xml files. By expressing text sizes in dp units, you get a certain amount of scaling automatically. I added them in the values xml files to provide a little bit more control and keep the text sizes more in line with the image sizes.

Figures 9-10: Text sizes in old and new Dashboard XL

The Dashboard XL app is a demo app that can be used as the starting point for a real application. If you do that, there is one more thing about the layout and values xml files that you should be aware of. There is a way to have a shared xml file in the layout folder so you don’t have to duplicate the file in all of your size-specific layout folders. Doing this worked out for me in my recently released Gomoku app. I used the resource structure described in this post. I needed a new layout to handle the landscape view of the game. I added a single layout.xml file in the values-land folder that looked like this, and then I put the one landscape_game.xml file in the regular layout folder.

<resources>
<item type="layout" name="game">
 @layout/landscape_game
 </item>
 ...
 </resources>

If you find yourself making duplicate copies of the same xml file in different layout folders, consider the item resource definition instead.

Source Code

The source code for this demo application is available in two places: (1) download from Google Docs; (2) download from Wglxy.com. The Google Docs zip file is a shared file. Still, some countries outside the US have trouble getting to that file. If you cannot download it from there, try the second location.

This application has been compiled with API level 10 (2.3.3). It runs in the emulator on devices ranging from API level 8 to API level 13. That includes virtual devices for Motorola Xoom (API 13) and Amazon Kindle Fire (API 10).  I also did some testing where I compiled with API level 15 (4.0.3); all the same virtual devices worked there also.

References

New Tools For Managing Screen Sizes – a blog article by Dianne Hackborn. It explains how to use the new size qualifiers (e.g. layout-600dp, layout-sw600dp). It also explains why you still need layout-large and layout-xlarge for tablets like the Galaxy Tab and Android Fire, which is at API level 10, where there is no support for the new tools.

Preparing for Handsets – blog article by Tim Bray. For me, the most useful tips here are the ones about transitioning from the tablet-only world of Honeycomb to Ice Cream Sandwich. It suggests compiling against 3.2 or later. This also helped me to understand that I can compile with the latest SDK and hit all my targets, which for me, has been API 8 and later.

Supporting Tablets and Handsets – provides guidance on supporting both worlds: single and multi-pane applications. As I’ve said, I have not started using Fragments yet, but this note is getting me ready. This one helped a lot because it’s what called my attention to the “New Tools” reference (above).

Android Compatibility – something I intend to come back to. It’s likely that I will need this for the older API levels I want to support after I convert to Fragments.

Supporting Multiple Screens – the reference that all the others refer to. It provides the detailed instructions on how to support different screen sizes.

Styles and Themes – always good to know this, particularly when you are ready to clean up your layout files and make them more maintainable.

Adapting My Android Dashboard UI To Tablets

(Note: As of March 12, 2012, this article is obsolete.. For a much improved way to support tablets, see “My Dashboard User Interface on Android Tablets“.)

If you are looking for a quick way to adapt your Android mobile phone app to a tablet, this post might save you a lot of time. I was facing that challenge about a month ago as I was nearing the release of my first Android app. I came up with a solution that gets your app ready for a tablet in a couple of hours rather than a couple of days.

The solution is not perfect, but often pretty good is good enough. It buys you time to work on a better solution.

A Pretty Good Solution for the Dashboard UI

To illustrate how this works, I went back to the work I had done on the Dashboard user interface: How To Build A Dashboard UI In Android. I built a new version of this demo and made it work on tablets.

The Dashboard demo on a regular device is shown to the right. Below is what it looks like after converting to a tablet. I call this the “Dashboard XL” demo. It appears as a 400×600 window inside the large screen.

Prior to implementing my solution, the old Dashboard demo simply expanded to fill the tablet’s screen. It is easy to see why this is far from an acceptable solution. And that’s just a home screen. In a real app, there are many supporting layouts.

The rest of this blog article explains how this works and how it helped me meet my deadline for releasing an app that included support for tablets.

A Pretty Good Solution Explained

Basically what I did was take the existing set of layouts for my app and arrange for them to be used without modification and run inside a small frame on the large tablet screen.

Placing an existing layout in a black frame isn’t too hard to do. If you have a layout that already looks good, you can simply add two more layouts around it. I used two layouts so I could specify both a background color for the tablet screen and a background for the area holding the app activity.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:orientation="vertical"
 android:background="@color/large_background"
 android:layout_gravity="center"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent">
 <LinearLayout
 android:id="@+id/frame"
 android:orientation="vertical"
 android:background="@color/title_background"
 android:layout_width="400dip"
 android:layout_height="600dip"
 android:layout_gravity="center">
 </LinearLayout>
...
</LinearLayout>

Notice that I name the inner layout with the id “frame”. I did this so I could write a little bit of code to detect Large or XLarge configurations and have that code add an existing layout below the layout named “frame”. Also note that the second layout has a width of 400 and a height of 600. (More on that choice of dimensions below.)

The code that adds to the “frame” layout is shown below. This code could have been inside of onCreate, but instead note that it is a override of the Activity’s setContentView method.

@Override public void setContentView (int layoutId)
{
 Configuration c = getResources ().getConfiguration ();
 int size = c.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
 boolean isLarge = (size == Configuration.SCREENLAYOUT_SIZE_LARGE);
 boolean isXLarge = (size == Configuration.SCREENLAYOUT_SIZE_XLARGE);
 boolean addFrame = isLarge || isXLarge;
 int finalLayoutId = addFrame ? R.layout.large : layoutId;
 super.setContentView (finalLayoutId);
 if (addFrame) {
   LinearLayout frameView = (LinearLayout) findViewById (R.id.frame);
   if (frameView != null) {
     // If the frameView is there, inflate the layout given as an argument.
     // Attach it as a child to the frameView.
     LayoutInflater li = ((Activity) this).getLayoutInflater();
     View childView = li.inflate (layoutId, null);
     if (childView != null) {
       LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams
             (ViewGroup.LayoutParams.MATCH_PARENT,
              ViewGroup.LayoutParams.MATCH_PARENT,
              1.0F);
       frameView.addView (childView, lp);
     }
    }
}

This requires some explanation.

The method starts out with checking to see if we are running on a Large or XLarge configuration. If we are, it means that we want to end up with our smaller frame inside the large screen of the tablet. There is a line that sets up the finalLayoutId. It gets set to R.layout.large if we are adding the  frame. Otherwise, it is the original layout id, which comes in as an argument. Notice the call to “super.setContentView”. Whatever case it is, that is what gets the activity’s views on the screen.

If it is the case that we are adding to the “frame” layout, we use a LayoutInflater to create the layout we will add as a child below the frane layout. Then we add it with the call to addView.

The other thing that worked out well is that I needed only one implementation of setContentView. All of the Activity classes could use the same method definition. If you’ve read the previous article on the Dashboard UI, you may recall that all the activities share a common superclass: DashboardActivity.

So the setContentView method above is actually defined in DashboardActivity. All the classes use it automatically because each one has an onCreate method that looks like this one for HomeActivity. That saved a lot of time.

protected void onCreate(Bundle savedInstanceState)
{
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_home);
}

In summary, here are the changes I made:

  • Dashboard activity superclass adds a setContentView method.
  • setContentView checks to see if the device has a Large or XLarge screen.
  • If the config is for Large or XLarges, setContentView uses a different layout that has been designed specifically for L or XL.
  • setContentView then embeds the original layout in that one.

Large and XLarge screens. I had to do a bit of experimentation to figure out a reasonable default size for the layout that displays on the tablet screen. I found rough guidelines about screen size on the Android Developers’ website. They talked about the range of screens supported in Android. I worked out something based on those sizes.

  • XLarge screens are at least 960dp x 720dp.
    For that, I used a 400 x 600 layout for Dashboard-XL.
  • Large screens are at least 640dp x 480dp.
    Dashboard-XL uses 400 x 600 and 600 x 400 for landscape.
I knew what 400 x 600 looked like before I started. Since that fit pretty well, I went with that. The only one I am not completely satisfied with is landscape for large screens. The wider screen makes it a bit harder to fit everything in without scrolling.

A Pretty Good Solution for Gomoku League

The reason I had for coming up with this solution was to finish an app that I was working on. The app is called “Gomoku League”. It runs on phones and on tablets. It is a full app with about 15 different full-screen layouts. Its home screen looks like this on a Motorola Xoom tablet.
(Click to enlarge)
If you interested in trying something on a tablet that goes beyond this dashboard demo, download the free Gomoku League app.

Source Code

Source code for this demo app can downloaded from the following places: (1) download zip file from Google Docs; (2) download zip file from Wglxy.com. Sometimes people outside the United States have trouble downloading the file from Google Docs. If you have trouble, use the alternate download location or try a second time. Often, access to Google Docs is intermittent.

Be sure to do a clean build in Eclipse after you import the project. Use the Project – Clean menu item in Eclipse.

This demo app was constructed using Android 2.3.3 (API level 10).

A Longer Version of My Story

At the start of this note, I told you the short version of my story about my app. Here’s the longer version.

I just recently released an app in the Android Market. It was something I had spent a fair amount of time on, but most of that effort was toward Android running on phones. As I got closer to release, I started thinking more about what I should do about Android tablets. I knew from a few quick tests in the emulator and some feedback I got from Beta test users that my app was not looking that good on tablets. I was faced with a choice. Should I simply forget about tablets for the first release, or should I figure out how to fix the user interface to be compatible with the larger screen sizes?

I decided to support tablets, but then I was faced with the problem of how to do that. I had this feeling that doing it right was going to take awhile. After all, that has been my experience the entire 12+ months that I have been learning the Android platform. Something that I first thought would be easy turns out to take a bit of effort to learn — worth the effort in the long run. This time, however, I did not want to think about the long run. I wanted my app out quickly.

So I looked for a solution that would get me to the Android Market as quickly as possible.

Conclusion

I know this is not a perfect solution, but I was in a hurry. I wanted to get my app out without adding a lengthy delay while I learned about supporting tablets the right way. Overall, I think it is a pretty good solution. I’d love to hear from other people on this.

(One last note: This blog is called “More Is Not Always Better” for a reason. I thought it would be good to show that it’s more than just a catchy title.  Sometimes a less than perfect solution is just the thing to do — at least for awhile)



Follow

Get every new post delivered to your Inbox.

Join 73 other followers