Android: 使用1个活动实现主细节流(双面板)

8
Android指南所述,可以通过两种方式实现双面板

  1. 多个片段,一个活动
  2. 多个片段,多个活动

我正在使用第一种情况(Android指南只解释了第二种情况)。

以下是在7英寸平板电脑上发生的情况:

  • 从横向旋转到纵向:只会重新创建单面板片段
  • 从纵向旋转到横向:所有3个片段(单面板、双面板主控、双面板详细信息)都会被重新创建

问题:为什么单面板片段(我通过以布局中定义的FrameLayout作为容器来编程创建)会在双面板上重新创建?

以下是我的实现:

/layout/activity_main.xml:

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

/layout-w900dp/activity_main.xml:

<LinearLayout
    android:id="@+id/dual_pane"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment class="com.example.MasterFragment"
        android:id="@+id/master_dual"
        android:tag="MASTER_FRAGMENT_DUAL_PANE"
        android:layout_width="@dimen/master_frag_width"
        android:layout_height="match_parent"/>
    <fragment class="com.example.DetailFragment"
        android:id="@+id/detail_dual"
        android:tag="DETAIL_FRAGMENT_DUAL_PANE"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

这是主活动中的onCreate

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mDualPane = findViewById(R.id.dual_pane)!=null;

    FragmentManager fm = getFragmentManager();
    if (savedInstanceState==null) {
        // this is a non-UI fragment I am using for data processing purposes
        fm.beginTransaction().add(new NonUiFragment(), DATA_FRAGMENT).commit();
    }
    if (!mDualPane && fm.findFragmentById(R.id.single_pane)==null) {
        fm.beginTransaction().add(R.id.single_pane, new MasterFragment(), MASTER_FRAGMENT_SINGLE_PANE).commit();
    }
}
2个回答

5

我发现对于双面板,将片段代码加入到代码中是更好的选择

因此,不要仅使用<fragment>,还应该在双面板XML中使用<FrameLayout>

/layout-w900dp/activity_main.xml:

<LinearLayout
    android:id="@+id/dual_pane"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:id="@+id/master_dual"
        android:layout_width="@dimen/master_frag_width"
        android:layout_height="match_parent"/>
    <FrameLayout
        android:id="@+id/detail_dual"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

以这种方式,您可以只使用一个 masterFragment 和 DetailFragment 实例,因此您不会遇到多个相同碎片实例的问题。
为了实现这一点,在 OnCreate 中,您需要将碎片添加到容器中,并从旧容器中分离:
    mDualPane = findViewById(R.id.dual_pane)!=null;

    if (savedInstanceState!=null) {
        mLastSinglePaneFragment = savedInstanceState.getString("lastSinglePaneFragment");
    }

    FragmentManager fm = getSupportFragmentManager();

    if (!mDualPane && fm.findFragmentById(R.id.single_pane)==null) {
        MasterFragment masterFragment = getDetatchedMasterFragment(false);
        fm.beginTransaction().add(R.id.single_pane, masterFragment, MASTER_FRAGMENT).commit();
        if (mLastSinglePaneFragment==DETAIL_FRAGMENT) {
            openSinglePaneDetailFragment();
        }
    }
    if (mDualPane && fm.findFragmentById(R.id.master_dual)==null) {
        MasterFragment masterFragment = getDetatchedMasterFragment(true);
        fm.beginTransaction().add(R.id.master_dual, masterFragment, MASTER_FRAGMENT).commit();
    }
    if (mDualPane && fm.findFragmentById(R.id.detail_dual)==null) {
        DetailFragment detailFragment = getDetatchedDetailFragment();
        fm.beginTransaction().add(R.id.detail_dual, detailFragment, DETAIL_FRAGMENT).commit();
    }

使用以下函数:

public static final String MASTER_FRAGMENT = "MASTER_FRAGMENT";
public static final String DETAIL_FRAGMENT = "DETAIL_FRAGMENT";

private MasterFragment getDetatchedMasterFragment(boolean popBackStack) {
    FragmentManager fm = getSupportFragmentManager();
    MasterFragment masterFragment = getSupportFragmentManager().findFragmentByTag(MASTER_FRAGMENT);
    if (masterFragment == null) {
        masterFragment = new MasterFragment();
    } else {
        if (popBackStack) {
            fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }
        fm.beginTransaction().remove(masterFragment).commit();
        fm.executePendingTransactions();
    }
    return masterFragment;
}

private DetailFragment getDetatchedDetailFragment() {
    FragmentManager fm = getSupportFragmentManager();
    DetailFragment detailFragment = getSupportFragmentManager().findFragmentByTag(DETAIL_FRAGMENT);
    if (detailFragment == null) {
        detailFragment = new DetailFragment();
    } else {
        fm.beginTransaction().remove(detailFragment).commit();
        fm.executePendingTransactions();
    }
    return detailFragment;
}

private void openSinglePaneDetailFragment() {
    FragmentManager fm = getSupportFragmentManager();
    fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
    DetailFragment detailFragment = getDetatchedDetailFragment();
    FragmentTransaction fragmentTransaction = fm.beginTransaction();
    fragmentTransaction.replace(R.id.single_pane, detailFragment, DETAIL_FRAGMENT);
    fragmentTransaction.addToBackStack(null);
    fragmentTransaction.commit();
}

0

当您旋转时,FragmentManager将保存当前活动的片段,并在创建新Activity时自动使用它们来重新创建片段。如果不将savedInstanceState传递给super方法,则可以通过此方式防止重新创建。例如:super.onCreate(null);

或者,如果您需要使用FragmentActivity.onCreate(savedInstanceState)方法(该方法调用FragmentManager.restoreAllState() ——请参见https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/FragmentManager.java#L1759),您可以查找您的片段标记并在onCreate中手动删除它。这种情况是因为您有一个非UI片段要恢复。保留片段的恢复还取决于对FragmentActivity.onCreate(savedInstanceState)的调用,其中saveInstanceState!= null。

重新创建发生的原因是通常您希望保留活动片段(并在平板电脑的情况下可能添加第二个详细面板)。

if (mDualPane) {
    Fragment singlePane = getFragmentManager().findFragmen‌​tByTag(MASTER_FRAGMENT_SINGLE_PANE);
    if (singlePane != null)
        getFragmentManager().beginTransaction().remove(fragment).commit(); 
}

嗨Pierre-Antoine,我意识到所有3个片段都由片段管理器保留。但是我不理解为什么单面片段应该在横向屏幕上重新创建(尽管其容器不在布局中),而双面片段则正确地在纵向屏幕上不会重新创建(因为它们不在布局中)。 - Daniele B
我本来想说你可以在没有UI的Fragment上调用setRetainInstance(true),但这似乎取决于对restoreAllState的调用。所以在这种情况下,当你检测到双面板时,你可能需要手动查找片段标记并执行remove()事务来删除片段。 - Pierre-Antoine LaFayette
请查看我的更新答案。对于您的情况,我不建议调用super.onCreate(null),因为您需要恢复状态。如果您要切换到双窗格,则只需查找单窗格片段并将其删除即可。 - Pierre-Antoine LaFayette
理想情况下,我希望单窗格片段的行为与双窗格片段类似。也就是说:始终保留在片段管理器中,无需显式删除,但同时不需要在不需要时重新创建。这是否不可能实现?为什么双窗格片段(未被删除!)不会在单窗格上重新创建,但单窗格片段会在双窗格上重新创建? - Daniele B
很有趣,可能是因为您在双窗格模式下的布局中定义了片段。我通常不会这样定义片段,所以我不确定。您是否尝试在纵向模式布局文件中定义单窗格而不是在代码中定义? - Pierre-Antoine LaFayette
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接