CHA1-Structure——7.处理配置信息的变化

原文:Handling Configuration Changes

—Screen Rotation


概述


存在这样一些情况:当屏幕方向旋转时,Activity实际上可以被销毁并从内存中移除,然后再从头重建一次。在这种情况下,最佳的处理方式是准备为将被重建的Activity,通过正确的方式保存并恢复状态。

保存并恢复Activity状态


当Activity将要停止时,系统调用onSaveInstanceState(),将Activity的状态信息保存在“键-值”对的集合中。该方法的默认实现会自动保存Activity的视图层次结构状态的信息,例如EditText组件中的文本或ListView的滚动位置。

要保存Activity的其他状态信息,必须要实现onSaveInstanceState(),并向Bundle对象中添加“键-值”对。例如:

public class MainActivity extends Activity {
    static final String SOME_VALUE = "int_value";
    static final String SOME_OTHER_VALUE = "string_value";

    @Override
    protected void onSaveInstanceState(Bundle savedInstanceState) {
        // 在bundle中保存自定义的value值
        savedInstanceState.putInt(SOME_VALUE, someIntValue);
        savedInstanceState.putString(SOME_OTHER_VALUE, someStringValue);
        // 总是调用父类的方法,来保存视图层次结构状态
        super.onSaveInstanceState(savedInstanceState);
    }
}

系统将在Activity被销毁前调用上述的方法,之后系统将会调用onRestoreInstanceState从bundle中恢复状态:

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    // 总是调用父类的方法,来恢复视图层次结构状态
    super.onRestoreInstanceState(savedInstanceState);
    // 从保存的实例中恢复状态成员
    someIntValue = savedInstanceState.getInt(SOME_VALUE);
    someStringValue = savedInstanceState.getString(SOME_OTHER_VALUE);
}

实例状态也可以在标准的Activity#onCreate方法中恢复,但在onRestoreInstanceState中操作起来更加方便,它可以确保所有的初始化都已完成,并允许子类决定是否使用默认实现。 更多细节请参阅 this stackoverflow post

注意onSaveInstanceStateonRestoreInstanceState方法不能保证一起被调用。Android系统在Activity有可能被销毁时调用onSaveInstanceState()。有些情况下,onSaveInstanceState()被调用,但是Activity并没有被销毁,因此onRestoreInstanceState未被调用。

更多内容请参考指南Recreating an Activity

保存并恢复Fragment状态


Fragment也有onSaveInstanceState方法,当它们的状态需要保存时该方法会被调用:

public class MySimpleFragment extends Fragment {
    private int someStateValue;
    private final String SOME_VALUE_KEY = "someValueToSave";
   
    // 当配置更改或Fragment需要保存状态时触发
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putInt(SOME_VALUE_KEY, someStateValue);
        super.onSaveInstanceState(outState);
    }
}

我们可以从onCreateView中将保存的数据抽取出来:

public class MySimpleFragment extends Fragment {
   // ...

   // 基于xml布局文件为Fragment填充视图
   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.my_simple_fragment, container, false);
        if (savedInstanceState != null) {
            someStateValue = savedInstanceState.getInt(SOME_VALUE_KEY);
            // 在必要时使用恢复的值
        }
        return view;
   }
}

为了正确地保存Fragment状态,我们必须确保当配置变化时,我们并不是在不必要地重建Fragment。这意味着当现有的Fragment已经存在时,不要再重新初始化。在Activity中初始化的任何Fragment,当配置变化之后,都要按标签查找

public class ParentActivity extends AppCompatActivity {
    private MySimpleFragment fragmentSimple;
    private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState != null) { // 保存实例状态,Fragment可能存在
           // 通过标签查找已存在的实例
           fragmentSimple = (MySimpleFragment)  
              getSupportFragmentManager().findFragmentByTag(SIMPLE_FRAGMENT_TAG);
        } else if (fragmentSimple == null) { 
           // 仅在Fragment还未被初始化的情况下创建它们
           fragmentSimple = new MySimpleFragment();
        }
    }
}

这就要求我们在使用事务将Fragment放入Activity中时,注意包含查找标签

public class ParentActivity extends AppCompatActivity {
    private MySimpleFragment fragmentSimple;
    private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 在这之上查找或初始化Fragment...
        // 对将要插入容器内的Fragment总是添加一个标签
        if (!fragmentSimple.isInLayout()) {
            getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.container, fragmentSimple, SIMPLE_FRAGMENT_TAG)
                .commit();
        }
    }
}

通过这种简单的模式,我们可以正确地重新使用Fragment,并在配置变化时恢复它们的状态。

保留Fragments


在很多情况下,我们可以通过简单地使用Fragment重新创建Activity,来避免问题的出现。如果你的视图和状态都在Fragment中,当Activity重建时我们就可以轻松地保留Fragment:

public class RetainedFragment extends Fragment {
    // 要保留的数据对象
    private MyDataObject data;

    // 该方法针对这个Fragment仅会被调用一次
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 当Activity重建时保留这个Fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

这种方法可以防止Fragment在Activity的生命周期内被破坏。它们保留在Fragment Manager中。更多信息请参阅Android 官方文档。

现在你可以在Fragment创建之前通过标签检测Fragment是否已存在,并且Fragment可以在配置变化期间保留它的状态。更多细节请参阅Handling Runtime Changes

正确处理列表状态


ListView

通常在旋转屏幕时,应用程序都将失去滚动位置和屏幕上列表的其他状态。要正确地保留ListView的状态,你可以在onPause中存储实例的状态并从onViewCreated中恢复,如下所示:

// YourActivity.java
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;

// 将List的状态值写入bundle
@Override
protected void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    mListState = getListView().onSaveInstanceState();
    state.putParcelable(LIST_STATE, mListState);
}

// 从bundle中恢复list的状态值
@Override
protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    mListState = state.getParcelable(LIST_STATE);
}


@Override
protected void onResume() {
    super.onResume();
    loadData(); // 首先要确保数据已经重新加载到适配器中
    // 一旦将数据项加载到适配器中,立即调用这一部分
    // 例如,网络请求的成功回调
    if (mListState != null) {
        myListView.onRestoreInstanceState(mListState);
        mListState = null;
    }
}

查看这篇博客文章和[stackoverflow文章](stackoverflow post](http://stackoverflow.com/a/5688490)了解更多细节。
 请注意,在调用onRestoreInstanceState之前,必须先将数据项加载到是配置中。换句话说,在数据未从网络或者数据库加载回来之前,请不要在ListView上调用onRestoreInstance

RecyclerView

通常在旋转屏幕时,应用程序都将失去滚动位置和屏幕上列表的其他状态。要正确地保留RecyclerView的状态,你可以在onPause中存储实例的状态并从onViewCreated中恢复,如下所示:

// YourActivity.java
public final static int LIST_STATE_KEY = "recycler_list_state";
Parcelable listState;

protected void onSaveInstanceState(Bundle state) {
     super.onSaveInstanceState(state);
     // 保存list状态
     listState = mLayoutManager.onSaveInstanceState();
     state.putParcelable(LIST_STATE_KEY, mListState);
}

protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    // 检索list状态和列表项的位置
    if(state != null)
        listState = state.getParcelable(LIST_STATE_KEY);
}

@Override
protected void onResume() {
    super.onResume();
    if (listState != null) {
        mLayoutManager.onRestoreInstanceState(listState);
    }
}

查看这篇博客文章stackoverflow文章 了解更多细节。

锁定屏幕方向


如果你想锁定应用中屏幕方向的变化,只需要在AndroidManifest.xml文件中给 <activity>标签设置android:screenOrientation属性即可:

<activity
    android:name="com.techblogon.screenorientationexample.MainActivity"
    android:screenOrientation="portrait"
    android:label="@string/app_name" >
    <!-- ... -->
</activity>

现在,Activity总是被强制以“竖屏”方式展示。

手动管理配置变化


如果你的应用在特定的配置变化时不需要更新资源,并且对性能方面有诸多限制,避免Activity重新启动,你可以声明由Activity自己处理配置变化,这将防止系统重新启动Activity。
 然而,这种技术应该被当做避免Activity在配置变化时重启的一种不得已而为之的方式,在大多数应用中并不推荐。采用这种方法,你必须添加android:configChanges节点到AndroidManifest.xml中的Activity中:

<activity android:name=".MyActivity"
          android:configChanges="orientation|screenSize|keyboardHidden"
          android:label="@string/app_name">

现在,当配置变化时Activity并不会重启,而是会收到一个onConfigurationChanged()的调用:

// 在收到这些变化的Activity中
// 检测当前设备的方向,相应地弹出提示
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);

    // 检测屏幕方向
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

参考Handling the Change文档. 更多处理Activity中配置变化的内容,请参阅 android:configChanges文档和Configuration类.

参考引用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容