处理屏幕旋转上的片段重复(带有示例代码)

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) 将通过旋转将实际片段保留在内存中并重用它。您的活动仍将被销毁并重新创建,没有办法阻止这种情况。旋转后,系统会将您的片段附加到新活动。另一种思考方式是“刷新您的片段正在使用的活动”。就像活动不能在旋转中存活一样,视图也不能,所以你的片段将得到onDestroyViewonCreateViewcall 以及它们之间的所有生命周期。注意:“片段的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();
        
    

【讨论】:

以上是关于处理屏幕旋转上的片段重复(带有示例代码)的主要内容,如果未能解决你的问题,请参考以下文章

使用 ActionBar 旋转 Android 的双片段

片段处理屏幕方向与操作栏中的选项卡

需要示例代码片段帮助

垂直于屏幕时,视图在 Y 轴上的旋转动画上剪辑

Android 片段不保存状态,在旋转/屏幕锁定/返回时崩溃

如何创建一个旋转的圆圈?