Posts Tagged 'how-to'

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).

Surprise Android Permission Requests

I found an interesting demo app on another blog and thought I would try it out on my phone. I downloaded the code, built the project, and then decided I’d package it as an apk file so I could share it easily in email. I sent it to myself and was ready to install it. I was a bit surprised to see a request for permissions. The app was requesting permission to modify SD card contents and to read phone state and identity.

Generally, it has been my policy not to install any Android app that asks for too much access to my Android device. Upon seeing this, I stopped. There was no reason for any permissions. A fix would certainly be easy, I thought.

I checked the Android manifest file, expecting to see those permissions, but there was nothing there.  At first, I suspected that there  was some alternate way to request permissions or cause these requests. It turned out that the answer was much simpler than that. Permissions introduced in newer  versions of Android are unknown to very old versions. If your app does not say where it can be used, the check is done with the assumption that you might be on one of those old versions.

So always specify the min SDK version in your manifest file. As soon as I added these lines in my manifest, installation was fine. No more permission requests.

<uses-sdk android:minSdkVersion="8" 
          android:targetSdkVersion="10"/>

The answer to this is out there in several places. The answer I found is this one: Why do so many Android apps require permissions to read phone state?  It took me awhile to find the answer so I’ve written it up here, as a reminder to myself and other developers regarding packaging apps for release.

(By the way, the demo app I was trying is quite fascinating. See Particle Explosion With Android for a really good demo app where particles explode and bounce around on the screen.)

What to do when your Android Emulator will not start from Eclipse

I do Android development work using Eclipse. I work on a MacBook. Like many people doing Android development in Eclipse, I often start an application to run in the Emulator and then see nothing happen. Everything starts out fine. You receive the first few reassuring messages in the console window and then nothing. You start with something like Figure 1 and you get stuck with something like Figure 2.

Figure 1 Starting Your App
(click to enlarge)

Figure 2 – Nothing Shows in the Android Device Chooser

In Figure 2, I show the Android Device Chooser window and Eclipse, waiting to connect. You have the Emulator device window. Here, I show it after I’ve touched the emulated device screen to get it to the home screen. However, notice that you do not see an application running, and no matter how long I wait, it won’t show up. What I used to do is keep starting new devices until I get one that starts up correctly. Occasionally, I would restart all of Eclipse. Lately — let’s say the last six months or so — I have found something that gets Eclipse properly connected to the emulator. Here’s what I do.

I start a Terminal app on my Mac. (I assume that using a CMD window in Windows would also work.) I type the following.

adb kill-server
adb devices

As soon as I issue the second command, the Android Device Chooser comes back to life and detects that indeed there is a device connected. This is shown in Figure 3 below. Also note that my test app continued its start up in the emulated device so there’s now an app running there.

Figure 3 – Killing the ADB server and restarting.

I know this obvious to many people out there. However, a Google search does not necessarily get you to an answer right away. By writing this note, I know I have the answer handy. One of the reasons I write articles on this blog is to make it easy for me to remember things.

The other thing that’s funny about this is that I swear that I have tried this many times in the last couple of years and it did not always work. Maybe I did not notice that it was the one thing that worked reliably, or maybe it did not work all the time until recently. Perhaps it depends on what version of Eclipse and Android Developer Tools (ADT)  and platform tools you have installed. I really don’t know, but thought I’d share my solution in case it works for others. My versions, by the way, are Eclipse Indigo and ADT version 16.0.1.

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.

How To Build A Simple Help Screen in Android

I released my first app to the Android Market recently. It was just complicated enough that I thought I should provide some help screens inside the app. I have taken what I did there and built a demo app, in order to illustrate how one can build a simple help screen in Android.

My simple Help screen consists of a summary page where there is an image and a brief description for each help topic. Touching the image takes you to the full help text for a topic. The figures below show what the two activities look like.

Figure 1 – Help screen

Figures 2-3 – Topic screen for topics 1 and 2

It is easy to build this kind of help screen. You need two layouts: one for the help activity and one for the topics. All of the help text comes from xml files in the resource folders. The help text can include paragraphs, text formatting (bold, italics), and even links to web pages. Even if you do not need a Help screen for your app, knowing how to have formatted Html text in an Android text view could be of use to you.

Help Activity

The demo app starts out in the HelpActivity. Its layout file is help.xml.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:background="@color/light_background"
 android:orientation="vertical" >
<ScrollView android:id="@+id/ScrollView1"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent">
<LinearLayout
 android:orientation="vertical"
 android:layout_width="fill_parent"
 android:layout_height="wrap_content"
 android:layout_weight="1"
 android:padding="6dip"
 >
<TextView
 android:id="@+id/help_page_intro"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="@string/help_page_intro"
 android:padding="2dip"
 android:layout_weight="1"
 />
...

</LinearLayout>
</ScrollView>
</LinearLayout>

The first thing to note is that it starts with a ScrollView that encloses a single LinearLayout. Doing that is all you need to have scrolling on the Help screen.

After the two items that support scrolling and the text view for the introduction, there are four LinearLayout views that use the horizontal orientation. Each one includes an image button and some summary text. This is what the first one looks like.

<LinearLayout
  android:orientation="horizontal"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:layout_weight="1"
  android:padding="4dip">
 <Button android:id="@+id/help_button1"
  android:layout_weight="1"
  android:layout_width="180dip"
  android:layout_height="wrap_content"
  style="@style/HelpButton.Dark"
  android:onClick="onClickHelp"
  android:text="@string/help_title_section1"
  android:drawableTop="@drawable/help_image1"/>
 <TextView
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/help_text_section1"
  android:padding="8dip"
  android:layout_weight="1"
  />
 </LinearLayout>

The image button has an image with text underneath it. The text comes from the resource string help_title_section1 (which has a value “Topics”). The drawable is defined by the help_image1 definition in the drawable folder. What makes this interesting is that it is not an image file, but an xml file.

<selector xmlns:android="http://schemas.android.com/apk/res/android">
 <item android:state_focused="true" android:state_pressed="true" 
    android:drawable="@drawable/light_bulb_pressed"/>
 <item android:state_focused="false" android:state_pressed="true"
    android:drawable="@drawable/light_bulb_pressed"/>
 <item android:state_focused="true" android:drawable="@drawable/light_bulb_selected"/>
 <item android:state_focused="false" android:state_pressed="false"
    android:drawable="@drawable/light_bulb_default"/>
</selector>

I used three distinct images so that the “selected” button looks different from the “pressed” button, and both are different from the button in its normal state. (For more on how to use these state lists, see StateListDrawable.)

Like other buttons, you can define the onClick handler in the xml file. In this case, the handler is onClickHelp. In fact, all four of the buttons use the same handler. The id of the view is used to determine which of the buttons was clicked.

The definition of onClickHelp is in HelpActivity. It looks at the id of the view to determine the resource id of the text to display in a TopicActivity. Note how it passes that argument as it starts the TopicActivity using an Intent.

public void onClickHelp (View v)
{
 int id = v.getId ();
 int textId = -1;
 switch (id) {
 case R.id.help_button1 :
   textId = R.string.topic_section1;
   break;
 case R.id.help_button2 :
   textId = R.string.topic_section2;
   break;
 case R.id.help_button3 :
   textId = R.string.topic_section3;
   break;
 case R.id.help_button4 :
   textId = R.string.topic_section4;
   break;
 default:
   break;
 }
 if (textId >= 0) startInfoActivity (textId);
 else toast ("Detailed Help for that topic is not available.", true);
}
public void startInfoActivity (int textId)
{
 if (textId >= 0) {
   Intent intent = (new Intent(this, TopicActivity.class));
   intent.putExtra (ARG_TEXT_ID, textId);
   startActivity (intent);
 }
}

Topic Activity

It may be a bit of a surprise to see how little code there is in the TopicActivity. It starts up and sets its content view. Then it reads the id of the string resource that is to be displayed on the screen. It locates the resource string and processes it as Html text and assigns that to the text view defined in the topic.xml.

protected void onCreate(Bundle savedInstanceState)
{
 super.onCreate(savedInstanceState);
 setContentView (R.layout.topic);
 // Read the arguments from the Intent object.
 Intent in = getIntent ();
 mTextResourceId = in.getIntExtra (HelpActivity.ARG_TEXT_ID, 0);
 if (mTextResourceId <= 0) mTextResourceId = R.string.no_help_available;
 TextView textView = (TextView) findViewById (R.id.topic_text);
 textView.setMovementMethod (LinkMovementMethod.getInstance());
 textView.setText (Html.fromHtml (getString (mTextResourceId)));
}

The line with the LinkMovementMethod is also important. Without that, links displayed on the screen are not active.

The Html.fromHtml method allows you to define paragraphs, bold, italics and links in the resource strings that define the help text. Figures 2-3 above showed what the formatted text looks like. The following is the string topic_section2 defined in the help_strings.xml file.

<string name="topic_section2">
 <![CDATA[
 <p>
 You can display simple forms of Html in a text view.
 You use the Html class and its fromHtml static method.
 See
 <a href="http://developer.android.com/reference/android/text/Html.html">Html
  on Android Developers</a>.
 </p>
 <p>
 As shown here, paragraphs are supported,
 as are <b>bold text</b> and <i>italics</i>.
 The preceding paragraph has a link in it.
 </p>
 <p><b>Return to the previous screen by touching the Back button.</b>
 </p>
 ]]>
 </string>

Source Code

The source code for this demo app can be downloaded from these two locations: (1) download source from Google Docs; (2) download source from Wglxy.com. The zip file at Google Docs is shared with everyone, but sometimes people outside the US have trouble downloading it. If you have problems, try the second location.

This demo app was built with Android API 10. Be sure to rebuild the project using the “Clean” command on the Build menu in Eclipse.

Integrating Help Into Your App

If you decide to you want a Help screen in your app, you can start with the code above. If you would like to have a form of context-sensitive help, you could consider doing what I did in my Gomoku League app.

In Gomoku League, I used the Help and Topic activities so I could explain how to play the game of Gomoku and how the league part of the app works. Gomoku League has a user interface based on the dashboard pattern. In the action bar for each section of the app, there is a “?” button that takes you directly to the help for that activity.

Even if you do not have an action bar at the top of the screen, you can find other ways to support context-sensitive help. The simplest is to include a Help item on the menu that you display when the user pushes the Menu button.

Viewing Android Source Code in Eclipse

This is a quick note to pass along a tip about viewing Android source code while you are working in Eclipse. If you’ve ever been frustrated with not knowing how the Android team did something in their code, you will really appreciate the Eclipse add-on for “Android Sources”. You can find it at http://code.google.com/p/adt-addons/. The installation instructions are about a screen or two down the page. The title of the section is “Android Sources”.

Having access to the source jar really makes debugging easier. As you check the stack to see how your methods were called, you will see source code for the Android classes.

Viewing Source In DebuggerViewing source of class

Just follow the instructions on the adt-addons page to install. When that finishes, it’s not really obvious that anything has happened. So you might want to check the installed software window in Eclipse. It should look like this:

Installed Software window

Another thing you can check is that the source code has been attached to the Android jar file. Go to one of your Android projects in Eclipse and find the Android jar. Expand that and pick a class within one of the packages. When you click the class, you should see the source code for the Android class. For my project, which uses Android 2.2, it looks like the following:

Screenshot of Android source in Eclipse

Thanks go to the ADT-addon people for their work. And thank you, members of the TriDroid meetup group, for sharing the tip.



Follow

Get every new post delivered to your Inbox.

Join 72 other followers