class 实例中的方法在重新创建带有片段的 Activity 之后保留旧的 Activity 引用(在设备方向更改时)
Method in class instance keeps old Activity references after Activity with Fragments is recreated (upon device orientation change)
今天我遇到了一个问题,我想与您分享,并就如何在我的应用程序中最好地解决这个问题征求您的意见。
上下文:
我想要的是有一个带有 ActionBar 和 Fragments 的布局。对于 Fragments,我使用 Android Studio 的默认代码,使用带有 SectionsPagerAdapter 的 ViewPager,其中填充了 PlaceholderFragments。不同的片段有一个 XML,并且我根据选项卡位置在 PlaceholderFragments class 的 onViewCreated() 覆盖中更改的一些 TextView 有所不同。此外,我希望 Fragments 在横向模式下的布局略有不同,因此我在 res/layout-land/ 目录中添加了一个 fragment_activity.xml。
在这个 Activity 中,我做了一个 class,在这个 class 中,我有一个名为 updateControlButtons():
的方法
public class SomeActivity extends ActionBarActivity implements ActionBar.TabListener
(...)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hangboard);
// Set up the action bar.
final ActionBar actionBar = getSupportActionBar();
Log.i("onCreate", actionBar.toString());
(...)
}
private class SomeClass {
public void updateControlButtons() {
(...)
runOnUiThread(new Runnable() {
public void run() {
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
Log.i("updateControlButtons", actionBar.toString());
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // KeepScreenOn off
(...)
}
}
(...)
}
(.....)
}
为了处理设备的方向变化,我必须将几个 TextView 重新初始化为当前选项卡,以及屏幕上正在发生的事情的当前状态(基于存储在 SomeClass 中的字段)。为此,我重写了 onViewCreated() 方法,如下所示:
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
//default code:
super.onViewCreated(view, savedInstanceState); //this does not really do something
int position = getArguments().getInt(ARG_SECTION_NUMBER);
Log.i("TEST onViewCreated position", Integer.toString(position));
switch (position) {
// no 0
case 1: //BEGINNER
someTextView.setText("0:05");
break;
(...)
}
// Reload the View IDs into the SomeClass if it is the current view created and SomeClass exists!
if (SomeClass != null && SomeClass.timerTab == position) {
updateCurrentViewIDs();
} // timerTab contains the Tab number on which the user started a timer, these values have to be restored
用updateCurrentViewIDs方法Activity:
private static void updateCurrentViewIDs() {
// for device reorientation
Log.i("updateCurrentViewIDs", "View re-initialised");
View mView = mViewPager.findViewWithTag(SomeClass.timerTab); // the View that was playing
SomeClass.mView = mView;
// findIDs for current tab
SomeClass.tvTimer = (TextView)mView.findViewById(R.id.tvTimer);
SomeClass.tvTimerRemaining = (TextView)mView.findViewById(R.id.tvTimerRemaining);
SomeClass.tvTimerTotal = (TextView)mView.findViewById(R.id.tvTimerTotal);
(..)
//update controls
SomeClass.updateControlButtons();
正确: TextView ID 正确更新为由 Fragment 重新创建的新视图(由于 Activity 重新创建),并且可以使用通过 SomeClass 仍然是 运行。
问题:
当我不更新甚至没有存储在 SomeClass 中的东西的 ID 时,SomeClass 仍然使用 Fragment 中 旧(和删除) 引用的 ID!例如,由新 Fragment(在 onViewCreated() 中)调用的 SomeClass.updateControlButtons() 仍然在 SomeClass.updateControlButtons() 中获得 ActionBar 的 old ID!这可以在 logcat:
中看到
I/onCreate﹕ android.support.v7.internal.app.WindowDecorActionBar@420f7570
I/TEST onViewCreated position﹕ 1
(..)
I/SomeClass.start()﹕ called
I/updateControlButtons﹕ android.support.v7.internal.app.WindowDecorActionBar@420f7570
(..)
(NOW DEVICE IS ORIENTED TO LANDSCAPE)
I/onCreate﹕ android.support.v7.internal.app.WindowDecorActionBar@421592d0
I/TEST onViewCreated position﹕ 1
I/updateCurrentViewIDs﹕ View re-initialised
I/updateControlButtons﹕ android.support.v7.internal.app.WindowDecorActionBar@420f7570
当然,当 SomeClass 实例关闭并创建新实例时,ActionBar 将再次被正确引用。
解决方案:当然,我可以简单地将ActionBar 全局存储在onCreate 方法中,并将其作为字段放在updateCurrentViewIDs() 方法中的SomeClass 中。然后我可以用更新后的字段替换 getSupportActionBar。我还必须为 Window 执行此操作才能正确调用:
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
顺便说一下,使用 setRetainInstance(true) 调用对我不起作用,因为我实际上想在横向模式下更改布局。
问题:我不喜欢这种方式,感觉好像我应该在设备的简单方向改变时更新太多。在我看来,某些东西甚至不应该(并且现在)不存储在 SomeClass 中?!难道不能用更简单或更优雅的方式解决这个问题吗?
我相信我通过深入研究 Recreating an Activity 找到了答案。
另外,根据documentation on Runtime Changes:
您的应用程序应该能够在不丢失用户数据或状态的情况下随时重新启动,以便处理诸如配置更改或用户收到传入 phone 呼叫和然后 returns 在您的应用程序进程可能已被销毁后很久才发送给您的应用程序。要了解如何恢复 activity 状态,请阅读 Activity 生命周期。
因此,我现在将研究如何使用 Bundle 在 Activity 重新创建时保存和恢复信息。
此外,我可能需要暂停当前的 SomeClass 实例,并在 Activity 恢复时创建一个新实例。然而,这是相当棘手的,因为我在 SomeClass 中有一个 for 循环 运行,所以还没有关于如何解决这个问题的线索......不过,我认为在方法内部调用 getSupportActionBar() 有点奇怪SomeClass 内部指的是最后一个(并被销毁)Activity 而不是新创建的 Activity,但这显然是如何编译的?
更新
我最终选择通过将它们存储在 Activity class 宽静态变量中来跟踪当前的 ActionBar 和 Window。我通过在 onCreate 方法中添加这些行来更新这些变量:
@Override
protected void onCreate(Bundle savedInstanceState) {
(..)
storedActionBar = actionBar; //PETER: added to be able to access the new ActionBar upon Activiy Recreate (e.g. orientation change)
storedWindow = getWindow(); // for the KEEP_SCREEN_ON flag
今天我遇到了一个问题,我想与您分享,并就如何在我的应用程序中最好地解决这个问题征求您的意见。
上下文: 我想要的是有一个带有 ActionBar 和 Fragments 的布局。对于 Fragments,我使用 Android Studio 的默认代码,使用带有 SectionsPagerAdapter 的 ViewPager,其中填充了 PlaceholderFragments。不同的片段有一个 XML,并且我根据选项卡位置在 PlaceholderFragments class 的 onViewCreated() 覆盖中更改的一些 TextView 有所不同。此外,我希望 Fragments 在横向模式下的布局略有不同,因此我在 res/layout-land/ 目录中添加了一个 fragment_activity.xml。 在这个 Activity 中,我做了一个 class,在这个 class 中,我有一个名为 updateControlButtons():
的方法public class SomeActivity extends ActionBarActivity implements ActionBar.TabListener
(...)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hangboard);
// Set up the action bar.
final ActionBar actionBar = getSupportActionBar();
Log.i("onCreate", actionBar.toString());
(...)
}
private class SomeClass {
public void updateControlButtons() {
(...)
runOnUiThread(new Runnable() {
public void run() {
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
Log.i("updateControlButtons", actionBar.toString());
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // KeepScreenOn off
(...)
}
}
(...)
}
(.....)
}
为了处理设备的方向变化,我必须将几个 TextView 重新初始化为当前选项卡,以及屏幕上正在发生的事情的当前状态(基于存储在 SomeClass 中的字段)。为此,我重写了 onViewCreated() 方法,如下所示:
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
//default code:
super.onViewCreated(view, savedInstanceState); //this does not really do something
int position = getArguments().getInt(ARG_SECTION_NUMBER);
Log.i("TEST onViewCreated position", Integer.toString(position));
switch (position) {
// no 0
case 1: //BEGINNER
someTextView.setText("0:05");
break;
(...)
}
// Reload the View IDs into the SomeClass if it is the current view created and SomeClass exists!
if (SomeClass != null && SomeClass.timerTab == position) {
updateCurrentViewIDs();
} // timerTab contains the Tab number on which the user started a timer, these values have to be restored
用updateCurrentViewIDs方法Activity:
private static void updateCurrentViewIDs() {
// for device reorientation
Log.i("updateCurrentViewIDs", "View re-initialised");
View mView = mViewPager.findViewWithTag(SomeClass.timerTab); // the View that was playing
SomeClass.mView = mView;
// findIDs for current tab
SomeClass.tvTimer = (TextView)mView.findViewById(R.id.tvTimer);
SomeClass.tvTimerRemaining = (TextView)mView.findViewById(R.id.tvTimerRemaining);
SomeClass.tvTimerTotal = (TextView)mView.findViewById(R.id.tvTimerTotal);
(..)
//update controls
SomeClass.updateControlButtons();
正确: TextView ID 正确更新为由 Fragment 重新创建的新视图(由于 Activity 重新创建),并且可以使用通过 SomeClass 仍然是 运行。
问题: 当我不更新甚至没有存储在 SomeClass 中的东西的 ID 时,SomeClass 仍然使用 Fragment 中 旧(和删除) 引用的 ID!例如,由新 Fragment(在 onViewCreated() 中)调用的 SomeClass.updateControlButtons() 仍然在 SomeClass.updateControlButtons() 中获得 ActionBar 的 old ID!这可以在 logcat:
中看到I/onCreate﹕ android.support.v7.internal.app.WindowDecorActionBar@420f7570
I/TEST onViewCreated position﹕ 1
(..)
I/SomeClass.start()﹕ called
I/updateControlButtons﹕ android.support.v7.internal.app.WindowDecorActionBar@420f7570
(..)
(NOW DEVICE IS ORIENTED TO LANDSCAPE)
I/onCreate﹕ android.support.v7.internal.app.WindowDecorActionBar@421592d0
I/TEST onViewCreated position﹕ 1
I/updateCurrentViewIDs﹕ View re-initialised
I/updateControlButtons﹕ android.support.v7.internal.app.WindowDecorActionBar@420f7570
当然,当 SomeClass 实例关闭并创建新实例时,ActionBar 将再次被正确引用。
解决方案:当然,我可以简单地将ActionBar 全局存储在onCreate 方法中,并将其作为字段放在updateCurrentViewIDs() 方法中的SomeClass 中。然后我可以用更新后的字段替换 getSupportActionBar。我还必须为 Window 执行此操作才能正确调用:
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
顺便说一下,使用 setRetainInstance(true) 调用对我不起作用,因为我实际上想在横向模式下更改布局。
问题:我不喜欢这种方式,感觉好像我应该在设备的简单方向改变时更新太多。在我看来,某些东西甚至不应该(并且现在)不存储在 SomeClass 中?!难道不能用更简单或更优雅的方式解决这个问题吗?
我相信我通过深入研究 Recreating an Activity 找到了答案。
另外,根据documentation on Runtime Changes:
您的应用程序应该能够在不丢失用户数据或状态的情况下随时重新启动,以便处理诸如配置更改或用户收到传入 phone 呼叫和然后 returns 在您的应用程序进程可能已被销毁后很久才发送给您的应用程序。要了解如何恢复 activity 状态,请阅读 Activity 生命周期。
因此,我现在将研究如何使用 Bundle 在 Activity 重新创建时保存和恢复信息。
此外,我可能需要暂停当前的 SomeClass 实例,并在 Activity 恢复时创建一个新实例。然而,这是相当棘手的,因为我在 SomeClass 中有一个 for 循环 运行,所以还没有关于如何解决这个问题的线索......不过,我认为在方法内部调用 getSupportActionBar() 有点奇怪SomeClass 内部指的是最后一个(并被销毁)Activity 而不是新创建的 Activity,但这显然是如何编译的?
更新 我最终选择通过将它们存储在 Activity class 宽静态变量中来跟踪当前的 ActionBar 和 Window。我通过在 onCreate 方法中添加这些行来更新这些变量:
@Override
protected void onCreate(Bundle savedInstanceState) {
(..)
storedActionBar = actionBar; //PETER: added to be able to access the new ActionBar upon Activiy Recreate (e.g. orientation change)
storedWindow = getWindow(); // for the KEEP_SCREEN_ON flag