1、准备环境
对模拟器,只要“Ctrl+F12“,就可以可以实现竖屏(portrait)和横屏(landscape)的切换。
2、UI的屏幕切换实现
下面一个简单的例子,如图。
我们需要写两个Android XML文件,假定文件为chapter_19_test1.xml,放在常规目录位置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/c19_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/c19_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:text="View"
android:enabled="false" />
</LinearLayout> |
另一个位于layout-land/目录下面,文件名称一样,这里是landsapce的排版,简单地,我们只作少许改动,将LinearLayout中的android:orientation="vertical",修改为android:orientation="horizontal",这样就可以了。
3、切换中出现什么事情
Is it so easy?在Android,关于layout widget,系统会根据你的xml文件自动进行屏幕旋转,选择正确的xml文件来进行布局。很智能吧?是的,但是仅此而已。我们做个实验:在上面xml的基础上,每按一次pick按钮,计数器testNum就加一,为了直观简单,testNum的值通过System.out打印处理,并在Button上直接显示。测试代码如下:
public class Chapter19Test1 extends Activity{
private int testNum = 0; //实验:跟踪testNum
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.chapter_19_test1);
Button pickButton = (Button)findViewById(R.id.c19_pick);
pickButton.setText("Pick : " + testNum);
pickButton.setOnClickListener(new View.OnClickListener() {
/*每按pick按键一次,计数器testNum就加一*/
public void onClick(View v) {
testNum ++;
System.out.println("Pick : " + testNum);
((Button)v).setText("Pick : " + testNum);
}
});
viewButton.setEnabled(contact != null);
}
} |
我们在竖屏的情况下按pick的按键,三次后,然后通过"ctrl+F12"从竖屏转为横屏,再按,发现testNum没有按照与其,继续计数,而是重新开始计算,而我们希望的是能保留原来的状。在屏幕的切换中Android会destroy并re-create我们的activity。这就意味这你需要保存你的数据并且在切换的时候从保存的数据中恢复状态。
4、保存数据
我们使用onSaveInstanceState()来进行数据保存,由于是re-create,在屏幕转换时会触发onCreate(),也可以在onRestoreInstanceState()中对数据进行恢复。
protected void onCreate(Bundle savedInstanceState) {
... ...
Button pickButton = (Button)findViewById(R.id.c19_pick);
/*从Bundle中获取保存的数据,进行恢复*/
if(savedInstanceState != null){
testNum = savedInstanceState.getInt("count");
}
pickButton.setText("Pick : " + testNum);
pickButton.setOnClickListener(new View.OnClickListener() {
......
});
}
@Override
/*通过onSaveInstanceState()来进行数据保存,存放如Bundle中。*/
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
outState.putInt("count", testNum);
} |
5、稍有趣一点的例子
在上面的例子中,我们按pick按钮,采用Intent方式进入contact列表,在列表中选择某个contact,并返回。通过startActivityForResult的调用方式,我们可以获得返回值。按View按钮,这可以显示该contact的详细信息。在这个例子中,我们学习下面的几个内容:
- 如何调用contact列表new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);,如何调用查看某个contact的详细内容new
Intent(Intent.ACTION_VIEW,contact)
- 如何调用带返回的intent:startActivityForResult(intent,requestCode);
- 在横竖屏切换中,复习保存数据(contact)并恢复原来状态。值得注意的是:下面的处理并不仅仅是为了横竖屏,还包括其他的恢复,例如因为电池能源太低而造成的activity的close。
public class Chapter19Test1 extends Activity{
static final int PICK_REQUEST=1000;
private Button viewButton = null;
private Uri contact = null;
protected void onCreate(Bundle savedInstanceState) {
... ...
pickButton.setOnClickListener(new View.OnClickListener() {
/* 下面给1)如何调用contact列表;2)如何调用代返回值的intent */
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
startActivityForResult(intent, PICK_REQUEST);
}
});
viewButton.setOnClickListener(new View.OnClickListener() {
/* 1)如何调用显示某个contact的详细内容*/
public void onClick(View v) {
startActivity(new Intent(Intent.ACTION_VIEW,contact));
}
});
restoreMe(savedInstanceState);
viewButton.setEnabled(contact != null);
}
@Override
/* 2)如何获取intent的返回数值,通过requestCode的匹配,确定是否我们所需*/
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == PICK_REQUEST){
if(resultCode == RESULT_OK){
contact = data.getData();
viewButton.setEnabled(true);
}
}
}
@Override
/* 3)用于屏幕旋转等情况的数据保存,这是系统自动调用的。*/
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(contact != null){
outState.putString("contact", contact.toString());
}
}
/* 3)在onCreate中调用,用与屏幕旋转时,恢复数据,其实还包括了其他的情况,例如电池电能太低*/
private void restoreMe(Bundle state){
contact = null;
if(state != null){
String contactUri = state.getString("contact");
if(contactUri != null){
contact = Uri.parse(contactUri);
}
}
}
} |
6、用onSaveInstanceState()保存数据的局限和替换方式
在例子中,使用了 onSaveInstanceState()来保存数据,这个调用不仅仅是在横屏竖屏的切换,同时也在其他引起中断的处理,例如低memory,因此数据保存不仅是串行进行的同时和当前之执行的进程无关。局限在于只能通过bundle来保存。
在一些activty的情况下,这样处理是没有问题,但是另一些,例如在线聊天,我们无法通过bundle将object(例如socket)保存起来,如果仅仅保存socket的信息(ip地址、端口)是没有意义的,也就是在横屏和竖屏的切换会导致连接的断开和重连接。显然不是我们期待的。
解决方法是用onRetainNonConfigurationInstance()来替代onSaveInstanceState()。用它来保存object,并通过getLastNonConfigurationInstance()来获取。这样我们可以保存socket,thread等等。上面的例子,可以修订如下:
public class Chapter19Test2 extends Activity{
... ...
@Override
protected void onCreate(Bundle savedInstanceState) {
... ...
/* 恢复数据 */
restoreMe();
... ...
}
... ...
@Override
/*通过onRetainNonConfigurationInstance()直接保存object,在之前的例子是保存了Uri的toString()信息*/
public Object onRetainNonConfigurationInstance() {
return contact;
}
/* 恢复数据,直接获得对象*/
private void restoreMe(){
contact = null;
if(getLastNonConfigurationInstance()!= null){
contact = (Uri)getLastNonConfigurationInstance();
}
}
} |
切换需注意数据保存和恢复
在上文横屏竖屏的切换中,我们配置了两个layout,一个用户普通的portrait,一个用户landsapce方式。如果只有一个layout,我们沿用上一个例子,删除了在layout-land/中的xml文件,则在屏幕切换时,会按照原来的排版,适配新的屏幕。程序我进行了简化,每按一次pick,就加一,用此来跟踪是否需要进行数据保存和恢复,如下:
public class Chapter19Test3 extends Activity{
private Button pickButton = null;
private int count = 0;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupViews();
}
private void setupViews(){
setContentView(R.layout.chapter_19_test1);
pickButton = (Button)findViewById(R.id.c19_pick);
pickButton.setText("Pick " + count);
pickButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
count ++;
pickButton.setText("Pick " + count);
}
});
}
} |
这是一个很简单的例子。我们发现在转屏的时候,是重新赋值。也就是也同样经过了destroy和create的阶段,同样需要进行保存和恢复。我们点击Pick按钮4次,通过模拟器Ctrl+F12进行切换,count重新归0。右边是另外两个例子,widget是一个输入框,我们可以看到如果我们修改了输入况中的值,旋转之后是不改变的,因为widget的内容,系统会帮我们处理,而widget外的我们需要处理。如果我们在例子中跟踪onCreate和onDestroy会发现,每次转动屏幕的时候,都会调用destroy,然后create。而且每次切换,destroy-create-destroy-create,但是我不确定是否只是模拟器,在实际target设备上是否也如此,可以看到切换后有一个明显UI的变更,可以解释为什么重新create两次。
不进行屏幕切换
由于某些原因,例如游戏,在切换屏幕的过程中,由于动作慢导致游戏失败,我们希望不触发屏幕切换。我们在AndroidManifest.xml中在activity进行设置:
<activity android:name=".xxxxx"
...... android:screenOrientation="portrait"
/>
即使我们在layout-land/补充了相关的layout的xml,也不会触发横屏和竖屏的布局的转换。
需要注意的是,我们跟踪发现,切换也同样会导致destroy->re-create,也就是需要进行数据的保存和恢复。由于Android的硬件设备类型很多,有些是通过smartkey,有些是通过物理键盘的打开,而在模拟器中就是“CTRL+F12”来强制进行屏幕切换。如果我们需要和iPhone那样有定位陀螺仪来触发屏幕的切换,我们只需在AndroidManifest.xml中<activity
android:name=".xxxxx" ...... android:screenOrientation="sensor"
/>即可。
自己控制旋转
我们如果希望自己控制屏幕选择,在收到相关的屏幕旋转的事件后,由程序自行控制处理,而不是系统自动处理。可以通过以下步骤:
一、在AndroidManifest.xml中的activity,增加android:configChanges属性,表示我们需要自行控制这些处理
<activity android:name=".xxx"
... android:configChanges="keyboardHidden|orientation"
/>
有一点比较奇特的就是,如果我们只设置orientation或者keyboardHidden,会有一些奇怪的现象,可能只出现在模拟器,因为CTRL+F12是模拟器的键盘操作。不确定实际设备会否如此。模拟器切换触发了key和orientation两个事件,我们现默认这种情况。
二、在程序中通过onConfigurationChanged()获得相关的事件并进行处理。
在切换的时候,系统,通过onConfigurationChanged()包括事件,系统将不会自行去处理界面,也就是不会因为重新描绘画面而对activity进行destroy和re-create,也就是最重要的,不会导致数据的重初始化而导致需对数据进行保存和恢复,仅此也是一种比较便捷的处理方式。
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setupViews();
} |
一般而且,我们可以通过newConfig来排断但前的状态,例如用switch(newConfig.orientation)来判断当前方向,而判断如何处理,在这个例子中,简单地重绘UI就可以了。
|