"Orphaned" 片段在旋转后消失

"Orphaned" fragment vanishes after rotation

UPDATE 问题是我一直在使用 getFragmentManager(),而不是在具有 sub-fragments.

的片段上使用 getChildFragmentManager()

问题仍然存在:我将如何处理 API < 17 中的深层嵌套片段?

原题

我的布局结构如下

Main Activity with layout main.xml 有一个永久保留的 fragment verytop,在 FrameLayout 中,fragment A (a.xml) 或 fragment B (b.xml)显示。您可以通过单击按钮在 A 和 B 之间切换(在 abottom_inner 中有一个按钮会调出 B,在 b 中有一个按钮会再次调出 A。)

只要我不旋转,一切正常。此外,如果我留在片段 A(不要单击按钮)并旋转,它也可以正常工作。但是如果我切换到 B,再回到 A,然后旋转,abottom_inner 是不可见的。

这是它的外观和启动方式(纵向模式,仅显示上半部分)

按下按钮后"show B":

按下 "show A" 后,看起来又像第一个屏幕截图。然后,在旋转到横向后,我得到这个

这里是 logcat 输出

***(start)
Main:          MAIN ONCREATE 
Main:          adding A (happens only at startup)
A:             onCreateView
A:             added atop
A:             added abottom
ATop:          onCreateView
ABottom:       onCreateView
ABottom:       added aBottomInner
ABottomInner:  onCreateView
ABottomInner:  a bottom inner button clicked
***(switch to B)
Main:          replacing A with B
B:             onCreateView
B:             b button clicked
***(switch back to A)
Main:          replacing B with A
A:             onCreateView
A:             added atop
A:             added abottom
ATop:          onCreateView
ABottom:       onCreateView
ABottom:       added aBottomInner
ABottomInner:  onCreateView
***(rotate to landscape)
Main:          MAIN ONCREATE 
Main:          content exists
A:             onCreateView
A:             atop already exists
A:             abottom already exists
ABottomInner:  onCreateView
ABottom:       onCreateView
ABottom:       aBottomInner already exists
ATop:          onCreateView

查看 logcat,我对行为原因的猜测是为每个子片段调用 onCreateView 方法的顺序。当它工作时(在启动时和单击按钮后),ABottomInner 的 onCreateView 在 ABottom 的 onCreateView 之后被调用。如果没有(旋转后,如果您之前单击过按钮),顺序就会颠倒过来。所以我的猜测是,在颠倒顺序的情况下,ABottomInner 变为 "orphaned" - 它依赖于之前调用的 ABottom 的 onCreateView,如果不是这样,它就无法正确附加自身。谁能证实或反驳我的猜测?另外,关于在旋转期间调用 onCreateView 方法的顺序是否有任何规则,还是只是随机的?看起来是这样,因为如果你在启动后立即旋转,顺序不会颠倒,片段仍然可见。

我有一个肮脏丑陋的解决方法,如果您选中 CheckBox,它就会被激活。然后,ABottomInner 将被重新创建,即使它已经存在。然后,它按预期工作。然后会调用 ABottomInner 的 onCreateView 两次。我无法想象这是这样做的正确方法。确保 abottom_inner 不会消失的正确方法是什么?

这是完整的代码。

main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:gravity="center_horizontal"
    android:orientation="vertical" >

    <fragment
        android:id="@+id/VeryTopFragment"
        android:name="com.example.nestedfrags.VeryTop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <FrameLayout
        android:id="@+id/contentFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />

</LinearLayout>

verytop.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@color/lime"
>

    <TextView
        android:id="@+id/veryTopTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=" ??? "
        android:layout_gravity="center_horizontal"
    />

</LinearLayout>

a.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >


    <FrameLayout
        android:id="@+id/aTop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/orange"
    />

    <FrameLayout
        android:id="@+id/aBottom"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/navy"
    />

</LinearLayout>

atop.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="@color/yellow"
>

    <TextView
        android:id="@+id/atopTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="---"
    />

</LinearLayout>

abottom.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/iamMain"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/silver">

    <CheckBox
        android:id="@+id/checkBox1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="CheckBox" />

    <FrameLayout
        android:id="@+id/abottomFL"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" >
    </FrameLayout>

</FrameLayout>

abottom_inner.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@color/white"
>

    <TextView
        android:id="@+id/aBottomInnerTV"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:textColor="@color/black"
        android:text=" ??? "
    />

    <Button
        android:id="@+id/aBotInnerButton"
        android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="show B" />
</LinearLayout>

b.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >


    <Button
        android:id="@+id/bButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="show A" />

</LinearLayout>

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="white">#FFFFFF</color>
    <color name="yellow">#FFFF00</color>
    <color name="silver">#C0C0C0</color>
    <color name="lime">#00FF00</color>
    <color name="navy">#000080</color>
    <color name="black">#000000</color>
    <color name="orange">#F7931E</color>
</resources>

MainActivity.java

package com.example.nestedfrags;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

    private static final String TAG = "Main";

    public VeryTop veryTop;

    Fragment contentFragment;

    public void showB(){
        FragmentManager fm = getFragmentManager();
        contentFragment = fm.findFragmentById(R.id.contentFragment);
        FragmentTransaction ft = fm.beginTransaction();
        B b = new B();
        // wouldn't it be nice if android were smart enough to remove "dependents" by itself?!
        ft.remove(fm.findFragmentById(R.id.aTop));
        ft.remove(fm.findFragmentById(R.id.aBottom));
        ft.remove(fm.findFragmentById(R.id.abottomFL));
        if (contentFragment == null){
            Log.i(TAG, "adding B");
            ft.add(R.id.contentFragment, b, "B").commit();
        } else {
            Log.i(TAG, "replacing A with B");
            ft.replace(R.id.contentFragment, b, "B").commit();
        }
    }
    public void showA(){
        FragmentManager fm = getFragmentManager();
        contentFragment = fm.findFragmentById(R.id.contentFragment);
        FragmentTransaction ft = fm.beginTransaction();
        if (contentFragment == null){
            Log.i(TAG, "adding A");
            ft.add(R.id.contentFragment, new A(), "A").commit();
        } else {
            Log.i(TAG, "replacing B with A");
            ft.replace(R.id.contentFragment, new A(), "A").commit();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i(TAG, " MAIN ONCREATE ");
        setContentView(R.layout.main);
        FragmentManager fm = getFragmentManager();
        contentFragment = fm.findFragmentById(R.id.contentFragment);
        veryTop = (VeryTop)fm.findFragmentById(R.id.VeryTopFragment);
        if (contentFragment == null){
            Log.i(TAG, "adding A (happens only at startup)");
            FragmentTransaction ft = fm.beginTransaction();
            ft.add(R.id.contentFragment, new A(), "A").commit();
        } else {
            Log.i(TAG, "content exists");
        }

    }
}

VeryTop.java

package com.example.nestedfrags;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class VeryTop extends Fragment {

    public boolean forceInnerRecreation = false;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
    public void updateTextView(View v){
        TextView tv = (TextView)v.findViewById(R.id.veryTopTV);
        tv.setText(" very top - forceInnerRecreation is: " + forceInnerRecreation);
    }
    public void setForceInnerRecreation(boolean value){
        forceInnerRecreation = value;
        updateTextView(getView());
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.verytop, container, false);
        updateTextView(v);
        return v;
    }

}

A.java

package com.example.nestedfrags;

import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class A extends Fragment {

    private static final String TAG = "A";

    private void incarnateTop(View v, FragmentManager fm){
        int layoutId = R.id.aTop;
        ATop fragment = (ATop)fm.findFragmentById(layoutId);
        boolean fragmentWasNull = false;
        if (fragment == null){
            fragment = new ATop();
            fragmentWasNull = true;
        }
        fragment.text = " == A TOP == "; 
        if (fragmentWasNull){
            FragmentTransaction ft = fm.beginTransaction();
            ft.add(layoutId, fragment, "atop").commit();
            Log.i(TAG, "added atop");
        } else {
            Log.i(TAG, "atop already exists");
        }
    }
    private void incarnateBottom(View v, FragmentManager fm){
        int layoutId = R.id.aBottom;
        ABottom fragment = (ABottom)fm.findFragmentById(layoutId);
        boolean fragmentWasNull = false;
        if (fragment == null){
            fragment = new ABottom();
            fragmentWasNull = true;
        }

        if (fragmentWasNull){
            FragmentTransaction ft = fm.beginTransaction();
            ft.add(layoutId, fragment, "abottom").commit();
            Log.i(TAG, "added abottom");
        } else {
            Log.i(TAG, "abottom already exists");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.a, container, false);
        Log.i(TAG, "onCreateView");
        FragmentManager fm = getFragmentManager();
        incarnateTop(v, fm);
        incarnateBottom(v, fm);
        return v;
    }

}

ATop.java

package com.example.nestedfrags;

import android.app.Fragment;
import android.app.FragmentManager;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class ATop extends Fragment {

    private static final String TAG = "ATop";

    String text = "invalid";

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.atop, container, false);
        Log.i(TAG, "onCreateView");
        FragmentManager fm = getFragmentManager();
        TextView tv = (TextView)v.findViewById(R.id.atopTV);
        tv.setText(text);
        return v;
    }

}

ABottom.java

package com.example.nestedfrags;

import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.FrameLayout;

public class ABottom extends Fragment {

    private static final String TAG = "ABottom";


    private void incarnateInner(View v, FragmentManager fm){
        int layoutId = R.id.abottomFL;
        FrameLayout container = (FrameLayout)v.findViewById(layoutId);
        container.setPadding(0, 200, 0, 0);
        ABottomInner fragment = (ABottomInner)fm.findFragmentById(layoutId);
        boolean fragmentWasNull = false;
        if (fragment == null){
            fragment = new ABottomInner();
            fragmentWasNull = true;
        }
        fragment.text = " -- a bottom inner -- ";
        if (fragmentWasNull){
            FragmentTransaction ft = fm.beginTransaction();
            ft.add(layoutId, fragment, "aBottomInner").commit();
            Log.i(TAG, "added aBottomInner");
        } else {
            if (forceInnerRecreation()){
                FragmentTransaction ft = fm.beginTransaction();
                fragment = new ABottomInner();
                fragment.text = " using brute workaround ";
                ft.replace(layoutId, fragment, "aBottomInner").commit();
                Log.i(TAG, "putting in fresh copy of aBottomInner");
            } else {
                Log.i(TAG, "aBottomInner already exists");
            }
        }
    }
    private boolean forceInnerRecreation(){
        MainActivity main = (MainActivity)getActivity();
        return main.veryTop.forceInnerRecreation;
    }
    private void setForceInnerRecreation(boolean value){
        MainActivity main = (MainActivity)getActivity();
        main.veryTop.setForceInnerRecreation(value);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.abottom, container, false);
        Log.i(TAG, "onCreateView");
        FragmentManager fm = getFragmentManager();
        CheckBox cb = (CheckBox)v.findViewById(R.id.checkBox1);
        cb.setChecked(forceInnerRecreation());
        OnCheckedChangeListener cbListener = new OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                setForceInnerRecreation(isChecked);
                if (isChecked){
                    Log.i(TAG, "setting brute workaround ON");
                } else {
                    Log.i(TAG, "setting brute workaround OFF");
                }
            }
        };
        cb.setOnCheckedChangeListener(cbListener);
        incarnateInner(v, fm);
        return v;
    }

}

ABottomInner.java

package com.example.nestedfrags;

import android.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

public class ABottomInner extends Fragment {

    private static final String TAG = "ABottomInner";

    String text = "invalid";

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.abottom_inner, container, false);
        Log.i(TAG, "onCreateView");
        TextView tv = (TextView)v.findViewById(R.id.aBottomInnerTV);
        tv.setText(text);
        Button btn = (Button)v.findViewById(R.id.aBotInnerButton);
        OnClickListener listener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "a bottom inner button clicked");
                MainActivity main = (MainActivity)getActivity();
                main.showB();
            }
        };
        btn.setOnClickListener(listener);
        return v;
    }
}

B.java

package com.example.nestedfrags;

import android.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.Button;

public class B extends Fragment {

    private static final String TAG = "B";

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Log.i(TAG, "onCreateView");
        View v = inflater.inflate(R.layout.b, container, false);
        Button bButton = (Button)v.findViewById(R.id.bButton);
        OnClickListener listener = new OnClickListener(){
            @Override
            public void onClick(View v) {
                Log.i(TAG, "b button clicked");
                MainActivity main = (MainActivity)getActivity();
                main.showA();
            }
        };
        bButton.setOnClickListener(listener);
        return v;
    }
}

感谢 Krish 的评论,我明白了。

[1] 在所有片段中,将 getFragmentManager 替换为 getChildFragmentManager。

[2] 删除表格 ft.remove(fm.findFragmentById(R.id. ... )) 的 3 行; 来自 MainActivity

然后就可以了。

实际上您是在另一个片段中呈现片段。所以你应该使用 getChildFragmentManager() 进行交易。否则会导致其他问题。

例如:对于 adding/replacing ABottom inside A 使用 getchildFragmentManager()