处理屏幕旋转上的片段重复(带有示例代码)
Posted
技术标签:
【中文标题】处理屏幕旋转上的片段重复(带有示例代码)【英文标题】:Handle Fragment duplication on Screen Rotate (with sample code) 【发布时间】:2013-03-07 22:37:34 【问题描述】:有一些类似的答案,但不是针对这种情况。
我的情况很简单。
我有一个具有两种不同布局的 Activity,一种是纵向的,另一种是横向的。
在Portrait中,我使用<FrameLayout>
并将Fragment
添加到其中动态。
在横向中,我使用<fragment>
,所以Fragment
是静态。 (其实这无关紧要)
它首先从纵向开始,然后我简单地添加了Fragment
:
ListFrag listFrag = new ListFrag();
getSupportFragmentManager().beginTransaction()
.replace(R.id.FragmentContainer, listFrag).commit();
ListFrag extends ListFragment
.
然后我做一个屏幕旋转。 我发现 listFrag 正在横向模式下重新创建。
(其中我注意到onCreate()
方法再次使用非空Bundle 调用)
我尝试像@NPike 在this post 中所说的那样使用setRetainInstance(false)
。但是getRetainInstance()
默认已经是false
。正如the docs 所说,它没有达到我的预期。谁能解释一下?
我正在处理的片段是ListFragment
,它在onCreate()
中执行setListAdapter()
。所以这里不能使用if (container == null) return null;
method。 (或者我不知道如何申请)。
我从this post 那里得到了一些提示。我应该在我的ListFragment
中使用if (bundle != null) setListAdapter(null); else setListAdapter(new ...);
吗?但是,有没有更好的方法在片段被销毁/分离时确实删除/删除片段,而不是在创建时执行它? (如if (container == null) return null;
方法)
编辑:
我发现的唯一巧妙方法是在onSaveInstanceState()
中执行getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentById(R.id.FragmentContainer)).commit();
。但这会引发另一个问题。
当屏幕被部分遮挡时,如 WhatsApp 或 TXT 对话框弹出,片段也会消失。 (这是相对较小的,只是视觉问题)
当屏幕旋转时,Activity 被完全销毁并重新创建。所以我可以在onCreate(Bundle)
或onRestoreInstanceState(Bundle)
中重新添加片段。但是在 (1) 的情况下,除了切换活动之外,当用户返回我的活动时,onCreate(Bundle)
和 onRestoreInstanceState(Bundle)
都不会被调用。我无处可重新创建 Activity(并从 Bundle 中检索数据)。
对不起,我没有说清楚,我已经做出了决定,getSupportFragmentManager()...replace(...).commit();
行仅在纵向模式下运行。
示例代码
我已经提取了简单的代码,以便更好地说明情况:)
MainActivity.java
package com.example.fragremovetrial;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
public class MainActivity extends FragmentActivity
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (findViewById(R.id.FragmentContainer) != null)
System.out.println("-- Portrait --"); // Portrait
ListFrag listFrag = new ListFrag();
getSupportFragmentManager().beginTransaction()
.replace(R.id.FragmentContainer, listFrag).commit();
else
System.out.println("-- Landscape --"); // Landscape
@Override
protected void onResume()
super.onResume();
if (findViewById(R.id.FragmentContainer) != null)
System.out.println("getRetainInstance = " +
getSupportFragmentManager().findFragmentById(R.id.FragmentContainer).getRetainInstance());
layout/activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/FragmentContainer"
android:layout_
android:layout_ />
layout-land/activity_main.xml (没关系)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_
android:layout_
android:orientation="vertical" >
</LinearLayout>
ListFrag.java
package com.example.fragremovetrial;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.widget.ArrayAdapter;
public class ListFrag extends ListFragment
private String[] MenuItems = "Content A", "Contnet B" ;
@Override
public void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
System.out.println("ListFrag.onCreate(): " + (savedInstanceState == null ? null : savedInstanceState));
setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, MenuItems));
注意我得到了以下内容
调试消息
-- 肖像-- ListFrag.onCreate(): null getRetainInstance = false(旋转端口 -> 陆地) ListFrag.onCreate(): 捆绑[android:view_state=android.util.SparseArray@4052dd28] -- 风景 -- 以前聚焦的视图在保存期间报告 id 16908298,但在恢复期间找不到。(旋转土地 -> 端口) ListFrag.onCreate(): 捆绑[android:view_state=android.util.SparseArray@405166c8] -- 肖像 -- ListFrag.onCreate(): null getRetainInstance = false(旋转端口 -> 陆地) ListFrag.onCreate(): Bundle[android:view_state=android.util.SparseArray@4050fb40] -- 风景 -- 以前聚焦的视图在保存期间报告 id 16908298,但在恢复期间找不到。(旋转土地 -> 端口) ListFrag.onCreate(): 捆绑[android:view_state=android.util.SparseArray@40528c60] -- 肖像 -- ListFrag.onCreate(): null getRetainInstance = false
创建的片段数量不会随着屏幕不断旋转而无限增加,我无法解释。 (请帮忙)
【问题讨论】:
在进一步阅读docs 之后,我想我明白为什么setRetainInstance(false)
在我的情况下不起作用。 (1)当setRetainInstance(true)
时,正如文档所说,它调用onDetach()
然后onAttach()
,并跳过onDestroy()
和onCreate()
。 (2) 因此,这意味着当setRetainInstance(false)
时,片段将是onDestroy()
,然后是onCreate(Bundle)
。这意味着setRetainInstance()
不是处理“保留/删除”片段的东西。它只控制“保留/重新创建”它。
对***.com/questions/15536679/…进行更结构化的检查
【参考方案1】:
处理此问题的正确方法是将以下内容放入您的 onCreate()
if (savedInstanceState == null)
// Do fragment transaction.
【讨论】:
如果片段已经在其他方向创建,则无论如何都会重新创建片段,无论您是否将在 OnCreate 中添加(替换)它与 (savedInstanceState == null) 。简单地说,查看日志 - 片段 onCreateView 和 onAttach 甚至在您的活动被调用 onCreate 之前就被调用了!! 是的,片段将在旋转时自动重新创建,尽管时间和地点无关紧要。如果您总是在 Activity 的onCreate
中添加片段,您将得到一个重复的片段。 savedInstanceState
只会在第一次创建活动时为空,并且在轮换后不会为空。因此,您只能在 savedInstanceState
为空时手动添加片段。也许我不明白你的评论。
是的,我完全理解这一点。但我认为在性能方面 - 将所有这些片段保存在内存中是否明智,即使它们不可见?有没有办法告诉片段管理器不要重新创建它们?或者允许片段直播是可以的,即使它不需要在屏幕上(例如在纵向模式下)
setRetainInstance(true)
将通过旋转将实际片段保留在内存中并重用它。您的活动仍将被销毁并重新创建,没有办法阻止这种情况。旋转后,系统会将您的片段附加到新活动。另一种思考方式是“刷新您的片段正在使用的活动”。就像活动不能在旋转中存活一样,视图也不能,所以你的片段将得到onDestroyView
和onCreateView
call 以及它们之间的所有生命周期。注意:“片段的onCreate
只会被调用一次,不会在轮换期间调用。
如果setRetainInstance(flase)
,那么在轮换期间,您的旧片段将被分离并被垃圾收集。因此它不会被保存在内存中。系统将在旋转后为您之前的片段创建一个全新的实例。您可以记录所有指针以观察这一点。据我所知,没有办法告诉系统不要在旋转时重新创建片段。不过,我想不出一个好的用例。【参考方案2】:
我终于想出了一个解决方案,以防止在 Activity 重新创建时重新创建不需要的 Fragment。就像我在问题中提到的那样:
我发现的唯一巧妙方法是在
onSaveInstanceState()
中执行getSupportFragmentManager().beginTransaction().remove(getSupportFragmentManager().findFragmentById(R.id.FragmentContainer)).commit();
。但它会引发另一个问题......
...有一些改进。
在onSaveInstanceState()
:
@Override
protected void onSaveInstanceState(Bundle outState)
if (isPortrait2Landscape())
remove_fragments();
super.onSaveInstanceState(outState);
private boolean isPortrait2Landscape()
return isDevicePortrait() && (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
isDevicePortrait()
就像:
private boolean isDevicePortrait()
return (findViewById(R.id.A_View_Only_In_Portrait) != null);
*请注意,我们不能使用getResources().getConfiguration().orientation
来确定设备当前是否是字面意思纵向。这是因为Resources
对象在RIGHT AFTER 屏幕旋转后发生了变化 - EVEN BEFORE onSaveInstanceState()
是叫!!
如果我们不想使用findViewById()
来测试方向(出于任何原因,而且它毕竟不是那么整洁),请保留一个全局变量private int current_orientation;
并在onCreate()
中用current_orientation = getResources().getConfiguration().orientation;
对其进行初始化。这看起来更整洁。但我们应该注意不要在 Activity 生命周期的任何地方更改它。
*请确保我们remove_fragments()
之前 super.onSaveInstanceState()
。
(因为在我的情况下,我从 Layout 和 Activity 中删除了 Fragments。如果它在 super.onSaveInstanceState()
之后,则 Layout 将已经保存到 Bundle 中。然后 Fragments 将 也在Activity重新创建后重新创建。###)
###我已经证明了这个现象。但What to determine a Fragment restore upon Activity re-create? 的原因只是我的猜测。如果您对此有任何想法,请回复my another question。
【讨论】:
【参考方案3】:在您的onCreate()
方法中,如果您处于纵向模式,您应该只创建您的ListFrag
。您可以通过检查您仅在纵向布局中拥有的 FrameLayout 视图是否不是 null
来做到这一点。
if (findViewById(R.id.yourFrameLayout) != null)
// you are in portrait mode
ListFrag listFrag = new ListFrag();
getSupportFragmentManager().beginTransaction()
.replace(R.id.FragmentContainer, listFrag).commit();
else
// you are in landscape mode
// get your Fragment from the xml
最后,如果您从纵向切换到横向布局,您不想创建另一个 ListFrag,而是使用您在 xml 中指定的 Fragment。
【讨论】:
感谢@Terry 的回复。但我已经做出了这个选择。修改我的问题的详细信息。对不起,我第一次没有说清楚。【参考方案4】:@midnite,首先感谢您的问题,我有同样的问题,我工作了好几天。现在我为这个问题找到了一些棘手的解决方案 - 并想与社区分享。 但是如果有人有更好的解决方案,请写在 cmets 中。
所以,我有同样的情况 - 两种设备方向的不同布局。纵向不需要DetailsFragment。
我只是在 OnCreate 中试图找到已经创建的片段:
fragment = (ToDoListFragment) getFragmentManager().findFragmentByTag(LISTFRAGMENT);
frameDetailsFragment = (FrameLayout) findViewById(R.id.detailsFragment);
detailsFragment = (DetailsFragment) getFragmentManager().findFragmentByTag(DETAILS_FRAGMENT);
settingsFragment = (SettingsFragment) getFragmentManager().findFragmentByTag(SETTINGS_FRAGMENT);
然后我添加我的主要片段 -
if (fragment == null)
fragment = new ToDoListFragment();
getFragmentManager().beginTransaction()
.add(R.id.container, fragment, LISTFRAGMENT)
.commit();
并清除我的碎片的任务:
if (getFragmentManager().getBackStackEntryCount() > 0 )
getFragmentManager().popBackStackImmediate();
最有趣的在这里 -
destroyTmpFragments();
这是方法本身:
private void destroyTmpFragments()
if (detailsFragment != null && !detailsFragment.isVisible())
Log.d("ANT", "detailsFragment != null, Destroying");
getFragmentManager().beginTransaction()
.remove(detailsFragment)
.commit();
detailsFragment = null;
if (settingsFragment != null && !settingsFragment.isVisible())
Log.d("ANT", "settingsFragment != null, Destroying");
getFragmentManager().beginTransaction()
.remove(settingsFragment)
.commit();
settingsFragment = null;
如您所见,我手动清理了 FragmentManager 为我轻轻保存的所有片段(非常感谢他)。 在日志中我有下一个生命周期调用:
04-24 23:03:27.164 3204 3204 D ANT MainActivity onCreate()
04-24 23:03:27.184 3204 3204 I ANT DetailsFragment :: onCreateView
04-24 23:03:27.204 3204 3204 I ANT DetailsFragment :: onActivityCreated
04-24 23:03:27.204 3204 3204 I ANT DetailsFragment :: onDestroy
04-24 23:03:27.208 3204 3204 I ANT DetailsFragment :: onDetach
所以在 onActivityCreated 中让您的视图检查是否为空 - 以防片段不可见。稍后会被删除(可能是因为片段管理器的删除是异步的) 之后,在活动的代码中,我们可以创建全新的 Fragment 实例,具有适当的布局(fargmnet 的占位符),并且 Fragment 将能够找到它的视图,例如,不会产生 NullPointerException
但是,在后台堆栈中的片段被删除得非常快,无需调用 onActivityCreated(在上面代码的帮助下:
if (getFragmentManager().getBackStackEntryCount() > 0 )
getFragmentManager().popBackStackImmediate();
最后,如果我在陆地方向,最后我会添加片段 -
if (frameDetailsFragment != null)
Log.i("ANT", "frameDetailsFragment != null");
if (EntryPool.getPool().getEntries().size() > 0)
if (detailsFragment == null)
detailsFragment = DetailsFragment.newInstance(EntryPool.getPool().getEntries().get(0), 0);
getFragmentManager().beginTransaction()
.replace(R.id.detailsFragment, detailsFragment, DETAILS_FRAGMENT)
.commit();
【讨论】:
以上是关于处理屏幕旋转上的片段重复(带有示例代码)的主要内容,如果未能解决你的问题,请参考以下文章