Chapter 19

Handling Rotation

Some Android handsets, like the T-Mobile G1, offer a slide-out keyboard that triggers rotating the screen from portrait to landscape orientation. Other handsets might use accelerometers to determine screen rotation, as the iPhone does. As a result, it is reasonable to assume that switching from portrait to landscape orientation and back again may be something your users will want to do.

As you'll learn in this chapter, Android has a number of ways for you to handle screen rotation, so your application can properly handle either orientation. But realize that these facilities just help you detect and manage the rotation process. You are still required to make sure you have layouts that look decent in each orientation.

A Philosophy of Destruction

By default, when there is a change in the phone configuration that might affect resource selection, Android will destroy and re-create any running or paused activities the next time they are to be viewed. While this could happen for a variety of different configuration changes (e.g., change of language selection), it is most likely to trip you up for rotations, since a change in orientation can cause you to load a different set of resources (e.g., layouts).

The key here is that this is the default behavior. It may even be the behavior that is best for one or more of your activities. You do have some control over the matter, though, and can tailor how your activities respond to orientation changes or similar configuration switches.

It's All the Same, Just Different

Since, by default, Android destroys and re-creates your activity on a rotation, you may only need to hook into the same onSaveInstanceState() that you would if your activity were destroyed for any other reason (e.g., low memory). Implement that method in your activity and fill in the supplied Bundle with enough information to get you back to your current state. Then, in onCreate() (or onRestoreInstanceState(), if you prefer), pick the data out of the Bundle and use it to bring your activity back to the way it was.

To demonstrate this, let's take a look at the Rotation/RotationOne project. This and the other sample projects in this chapter use a pair of main.xml layouts: one in res/layout/ and one in res/layout-land/ for use in landscape mode. Here is the portrait layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  >
  <Button android:id="@+id/pick"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    android:text="Pick"
    android:enabled="true"
  />
  <Button android:id="@+id/view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    android:text="View"
    android:enabled="false"
  />
</LinearLayout>

Here is the similar landscape layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="horizontal"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  >
  <Button android:id="@+id/pick"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    android:text="Pick"
    android:enabled="true"
  />
  <Button android:id="@+id/view"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    android:text="View"
    android:enabled="false"
  />
</LinearLayout>

Basically, the layout contains a pair of buttons, each taking up half the screen. In portrait mode, the buttons are stacked; in landscape mode, they are side by side.

If you were to simply create a project, put in those two layouts, and compile it, the application would appear to work just fine—a rotation (pressing Ctrl+F12 in the emulator) will cause the layout to change. And while buttons lack state, if you were using other widgets (e.g., EditText), you would even find that Android hangs onto some of the widget state for you (e.g., the text entered in the EditText).

What Android cannot help you with automatically is anything held outside the widgets.

This application lets you pick a contact, and then view the contact, via separate buttons. The View button is enabled only after a contact has been selected.

Let's see how we handle this, using onSaveInstanceState():

public class RotationOneDemo extends Activity {
  static final int PICK_REQUEST=1337;
  Button viewButton=null;
  Uri contact=null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button btn=(Button)findViewById(R.id.pick);

    btn.setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        Intent i=new Intent(Intent.ACTION_PICK,
                            Contacts.CONTENT_URI);

        startActivityForResult(i, PICK_REQUEST);
      }
    });

    viewButton=(Button)findViewById(R.id.view);

    viewButton.setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        startActivity(new Intent(Intent.ACTION_VIEW, contact));
      }
    });

    restoreMe(savedInstanceState);

    viewButton.setEnabled(contact!=null);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
                                   Intent data) {
    if (requestCode==PICK_REQUEST) {
      if (resultCode==RESULT_OK) {
        contact=data.getData();
        viewButton.setEnabled(true);
      }
    }
  }

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    if (contact!=null) {
      outState.putString("contact", contact.toString());
    }
  }

  private void restoreMe(Bundle state) {
    contact=null;

    if (state!=null) {
      String contactUri=state.getString("contact");
      
      if (contactUri!=null) {
        contact=Uri.parse(contactUri);
      }
    }
  }
}

By and large, it looks like a normal activity (because it is). Initially, the “model”—a Uri named contact—is null. It is set as the result of spawning the ACTION_PICK subactivity. Its string representation is saved in onSaveInstanceState() and restored in restoreMe() (called from onCreate()). If the contact is not null, the View button is enabled and can be used to view the chosen contact.

Visually, it looks pretty much as you would expect, as shown in Figures 19–1 and 19–2.

image

Figure 19–1. The RotationOne application, in portrait mode

image

Figure 19–2. The RotationOne application, in landscape mode

The benefit to this implementation is that it handles a number of system events beyond mere rotation, such as being closed by Android due to low memory.

For fun, comment out the restoreMe() call in onCreate() and try running the application. You will see that the application “forgets” a contact selected in one orientation when you rotate the emulator or device.

note: All the samples for this chapter work only on Android 2.0 and higher, as they use the newer means of picking a contact from the Contacts content provider (discussed in Chapter 26).

Now with More Savings!

The problem with onSaveInstanceState() is that you are limited to a Bundle. That's because this callback is also used in cases where your whole process might be terminated (e.g., low memory), so the data to be saved must be something that can be serialized and does not have any dependencies on your running process.

For some activities, that limitation is not a problem. For others, it is more annoying. Take an online chat, for example. You have no means of storing a socket in a Bundle, so by default, you will need to drop your connection to the chat server and reestablish it. That not only may be a performance hit, but it might also affect the chat itself, such as appearing in the chat logs as disconnecting and reconnecting.

One way to get past this is to use onRetainNonConfigurationInstance() instead of onSaveInstanceState() for “light” changes like a rotation. Your activity's onRetainNonConfigurationInstance() callback can return an Object, which you can retrieve later via getLastNonConfigurationInstance(). The Object can be just about anything you want. Typically, it will be some kind of “context” object holding activity state, such as running threads, open sockets, and the like. Your activity's onCreate() can call getLastNonConfigurationInstance(). Then if you get a non-null response, you now have your sockets and threads and whatnot. The biggest limitation is that you do not want to put in the saved context anything that might reference a resource that will get swapped out, such as a Drawable loaded from a resource.

Let's take a look at the Rotation/RotationTwo sample project, which uses this approach to handling rotations. The layouts, and hence the visual appearance, is the same as with Rotation/RotationOne. Where things differ slightly is in the Java code:

public class RotationTwoDemo extends Activity {
  static final int PICK_REQUEST=1337;
  Button viewButton=null;
  Uri contact=null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button btn=(Button)findViewById(R.id.pick);

    btn.setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        Intent i=new Intent(Intent.ACTION_PICK,
                            Contacts.CONTENT_URI);

        startActivityForResult(i, PICK_REQUEST);
      }
    });

    viewButton=(Button)findViewById(R.id.view);

    viewButton.setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        startActivity(new Intent(Intent.ACTION_VIEW, contact));
      }
    });

    restoreMe();

    viewButton.setEnabled(contact!=null);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
                                   Intent data) {
    if (requestCode==PICK_REQUEST) {
      if (resultCode==RESULT_OK) {
        contact=data.getData();
        viewButton.setEnabled(true);
      }
    }
  }

  @Override
  public Object onRetainNonConfigurationInstance() {
    return(contact);
  }

  private void restoreMe() {
    contact=null;

    if (getLastNonConfigurationInstance()!=null) {
      contact=(Uri)getLastNonConfigurationInstance();
    }
  }
}

In this case, we override onRetainNonConfigurationInstance(), returning the actual Uri for our contact, rather than a string representation of it. In turn, restoreMe() calls getLastNonConfigurationInstance(), and if it is not null, we hold onto it as our contact and enable the View button.

The advantage here is that we are passing around the Uri rather than a string representation. In this case, that is not a big saving. But our state could be much more complicated, including threads, sockets, and other things we cannot pack into a Bundle.

However, even this approach may be too intrusive to your application. Suppose, for example, you are creating a real-time game, such as a first-person shooter. The “hiccup” your users experience as your activity is destroyed and re-created might be enough to get them shot, which they may not appreciate. While this would be less of an issue on the T-Mobile G1, since a rotation requires sliding open the keyboard and therefore is unlikely to be done mid-game, other devices might rotate based solely on the device's position as determined by accelerometers. For these situations, you may want to tell Android that you will rotations yourself, and you do not want any assistance from the framework, as described next.

DIY Rotation

To handle rotations on your own, do this:

  1. Put an android:configChanges entry in your AndroidManifest.xml file, listing the configuration changes you want to handle yourself versus allowing Android to handle for you.
  2. Implement onConfigurationChanged() in your Activity, which will be called when one of the configuration changes you listed in android:configChanges occurs.

Now, for any configuration change you want, you can bypass the whole activity-destruction process and simply get a callback letting you know of the change.

To see this in action, turn to the Rotation/RotationThree sample application. Once again, our layouts are the same, so the application looks just like the preceding two samples. However, the Java code is significantly different, because we are no longer concerned with saving our state, but rather with updating our UI to deal with the layout.

But first, we need to make a small change to our manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.commonsware.android.rotation.three"
      android:versionCode="1"
      android:versionName="1.0.0">
  <uses-sdk
        android:minSdkVersion="5"
        android:targetSdkVersion="6"
    />
    <application android:label="@string/app_name"
        android:icon="@drawable/cw">
        <activity android:name=".RotationThreeDemo"
                  android:label="@string/app_name"
                  android:configChanges="keyboardHidden|orientation">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Here, we state that we will handle keyboardHidden and orientation configuration changes ourselves. This covers us for any cause of the rotation, whether it is a sliding keyboard or a physical rotation. Note that this is set on the activity, not the application. If you have several activities, you will need to decide for each which of the tactics outlined in this chapter you wish to use.

The Java code for this project is as follows:

public class RotationThreeDemo extends Activity {
  static final int PICK_REQUEST=1337;
  Button viewButton=null;
  Uri contact=null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setupViews();
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
                                   Intent data) {
    if (requestCode==PICK_REQUEST) {
      if (resultCode==RESULT_OK) {
        contact=data.getData();
        viewButton.setEnabled(true);
      }
    }
  }

  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    setupViews();
  }

  private void setupViews() {
    setContentView(R.layout.main);

    Button btn=(Button)findViewById(R.id.pick);

    btn.setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        Intent i=new Intent(Intent.ACTION_PICK,
                           Contacts.CONTENT_URI);

        startActivityForResult(i, PICK_REQUEST);
      }
    });

    viewButton=(Button)findViewById(R.id.view);

    viewButton.setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        startActivity(new Intent(Intent.ACTION_VIEW, contact));
      }
    });

    viewButton.setEnabled(contact!=null);
  }
}

The onCreate() implementation delegates most of its logic to a setupViews() method, which loads the layout and sets up the buttons. This logic was broken out into its own method because it is also called from onConfigurationChanged().

Forcing the Issue

In the previous three sections, we covered ways to deal with rotational events. There is, of course, a radical alternative: tell Android not to rotate your activity at all. If the activity does not rotate, you do not need to worry about writing code to deal with rotations.

To block Android from rotating your activity, all you need to do is add android:screenOrientation = “portrait” (or “landscape”, as you prefer) to your AndroidManifest.xml file, as follows (from the Rotation/RotationFour sample project):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.commonsware.android.rotation.four"
      android:versionCode="1"
      android:versionName="1.0.0">
  <uses-sdk
        android:minSdkVersion="5"
        android:targetSdkVersion="6"
    />
    <application android:label="@string/app_name"
        android:icon="@drawable/cw">
        <activity android:name=".RotationFourDemo"
                  android:screenOrientation="portrait"
                  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>
</manifest>

Since this is applied on a per-activity basis, you will need to decide which of your activities may need it turned on.

At this point, your activity is locked into whatever orientation you specified, regardless of what you do. Figures 19–3 and 19–4 show the same activity as in the previous three sections, but using the preceding manifest and with the emulator set for both portrait and landscape orientation. Notice that the UI does not move a bit, but remains in portrait mode.

image

Figure 19–3. The RotationFour application, in portrait mode

image

Figure 19–4. The RotationFour application, in landscape mode

Note that Android will still destroy and re-create your activity, even if you have the orientation set to a specific value as shown here. If you wish to avoid that, you will also need to set android:configChanges in the manifest, as described earlier in this chapter.

Making Sense of It All

All of the scenarios presented in this chapter assume that you rotate the screen by opening the keyboard on the device (or by pressing Ctrl+F12 in the emulator). Certainly, this is the norm for Android applications. However, we haven't covered the iPhone scenario.

You may have seen one (or several) commercials for the iPhone, showing how the screen rotates just by turning the device. Some Android devices, such as the HTC Magic, will behave the same way. With other devices, though, you do not get this behavior; instead, the screen rotates based on whether the keyboard is open or closed.

However, even for those devices, it is easy for you to change this behavior, so your screen will rotate based on the position of the phone. Just add android:screenOrientation = “sensor” to your AndroidManifest.xml file (from the Rotation/RotationFive sample project):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.commonsware.android.rotation.five"
      android:versionCode="1"
      android:versionName="1.0.0">
  <uses-sdk
        android:minSdkVersion="5"
        android:targetSdkVersion="6"
    />
    <application android:label="@string/app_name"
        android:icon="@drawable/cw">
        <activity android:name=".RotationFiveDemo"
                  android:screenOrientation="sensor"
                  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>
</manifest>

The sensor, in this case, tells Android you want the accelerometers to control the screen orientation, so the physical shift in the device orientation controls the screen orientation.

At least on the T-Mobile G1, this appears to work only when going from the traditional upright portrait position to the traditional landscape position—rotating 90 degrees counterclockwise. Rotating the device 90 degrees clockwise results in no change in the screen.

Also note that this setting disables having the keyboard trigger a rotation event. Leaving the device in the portrait position, if you slide out the keyboard, in a normal Android activity, the screen will rotate; in an android:screenOrientation = “sensor” activity, the screen will not rotate.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.248.0