Android 4
15. Configuring Rotation
This chapter
- 15.0 Rotation
- 15.1 Using onSaveInstanceState()
- 15.2 Using onRetainNonConfigurationInstance()
- 15.3 Using onConfiguratuionChanged()
Use of this code assumes agreement with the Google Custom Search Terms of Service. The terms of service are available at //www.google.com/cse/docs/tos.html
Android has a number of ways for us to handle screen rotation, so our application can properly handle orientation, portrait or landscape. But these just help us detect and manage the rotation process. So, we are still required to make sure we have layouts that look decent in each orientation.
Android, by default, destroys and recreates our activity on a rotation, we may only need to hook into the same onSaveInstanceState() if our activity were destroyed. Implement that method in our activity and fill in the supplied Bundle with enough information to get us back to our current state. Then, in onCreate() pick the data out of the Bundle and use it to bring our activity back to the way it was.
Let's look at the two layouts we're going to use.
The first layout: res/layout/main.xml
<?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="@string/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="@string/view" android:enabled="false"/> </LinearLayout>
The second layout: res/layout-land/main.xml
<?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="@string/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="@string/view" android:enabled="false"/> </LinearLayout>
strings.xml:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, RotationA!</string> <string name="app_name">RotationA</string> <string name="view">View</string> <string name="pick">Pick</string> </resources>
The two layouts are essentially the same except the lines for android:orientation.
The layout contains a pair of buttons, each taking up half of the screen. In portrait mode, the buttons are stacked while in landscape more, they are side by side.
Any rotation change, Ctrl+F12 for emulator, will cause the layout to change.
Our application lets us pick a contact, and then view the contact, via separate buttons. The View button is enabled only after a contact has been selected.
package com.bogotobogo.rotationA; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract.Contacts; import android.view.View; import android.widget.Button; public class RotationA 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); } } } }
Initially, a Uri named contact is null. It is set as the result of spawning the ACTION_PICK subactivity.
public void onClick(View view) { Intent i=new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI); startActivityForResult(i, PICK_REQUEST); }
Its string representation is saved in onSaveInstanceState() and restored in restoreMe() which is called from onCreate(). If the contact is not null, the View button is enabled and can be used to view the chosen contact.
The problem with onSaveInstanceState() is that we are limited to a Bundle. That's because this callback is also used in cases where our whole process might be terminated. So the data to be saved must be something that can be serialized and does not have any dependencies on our running process.
One way to get past this is to use onRetainNonConfigurationInstance() instead of onSaveInstanceState() for light changes like a rotation. Our activity's onRetainNonConfigurationInstance() callback can return an Object, which we can retrieve later via getLastNonConfigurationInstance().
Here is our Java code:
package com.bogotobogo.rotationB; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract.Contacts; import android.view.View; import android.widget.Button; 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(); } } }
We override onRetainNonConfigurationInstance(), returning the actual Uri for our contact, rather than a string.
@Override public Object onRetainNonConfigurationInstance() { return(contact); }
In turn, restoreMe() calls getLastNonConfigurationInstance(). Then if we get a non-null response, we hold onto it as our contact
private void restoreMe() { contact=null; if (getLastNonConfigurationInstance()!=null) { contact=(Uri)getLastNonConfigurationInstance(); } }and enable the View button in onActivityResult().
if (requestCode==PICK_REQUEST) { if (resultCode==RESULT_OK) { contact=data.getData(); viewButton.setEnabled(true); } }
The advantage here is that we are passing around the Uri instead of a string. That could be a big saving when our state is much more complicated including threads, sockets, and other things we cannot pack into a Bundle.
But this approach may be too intrusive to our application. So, we may want to tell Android that we'll do our rotation, and we do not want any assistance from the framework as described in the next section.
To handle rotations on our own, we need to do this:
- Put an android:configChanges entry in our AndroidManifest.xml file, listing the configuration changes we want to handle ourselves without allowing Android to handle for us.
- Implement onConfiguratuionChanged() in our Activity, which will be called when one of the configuration changes we listed in android:configureChanges occurs.
Now we can bypass the activity destruction process at any configuration change, and we simply get a callback letting us know of the change.
In this example, as in the previous two examples, we're using the same layouts. But our manifest and Java source code require some changes. We are no longer concerned with saving our state, but rather with updating our UI to deal with the layout.
Manifest file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bogotobogo.rotationc" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="14" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".RotationC" 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.
android:configChanges="keyboardHidden|orientation">
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.
And the Java code:
package com.bogotobogo.rotationC; import android.app.Activity; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract.Contacts; import android.view.View; import android.widget.Button; 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
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setupViews(); }which loads the layout and sets up the buttons.
This logic was broken into its own method because it is also called from onConfigurationChanged().
public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setupViews(); }
The results are the same as the other cases (rotationA and rotationB):
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization