从单窗格到双窗格Fragment布局改变方向的问题



我有一个包含多个fragment的Activity。在平板电脑(720dp或更宽)上,我会在左侧的Fragment中显示一个列表,然后在右侧的窗格中显示多个Fragment中的一个。当用户从列表中选择不同的项目时,右窗格中的Fragment会发生变化。在单窗格模式下,用户将看到Fragment列表,点击一个,然后Fragment列表将被替换为detail Fragment。

这里有一个棘手的部分:以7英寸的平板电脑为例,横向屏幕的宽度足以容纳两个面板,而纵向屏幕的宽度只能容纳一个。在单窗格模式和双窗格模式之间旋转。有些东西,如操作栏图标和标题,需要更新,但这并不太复杂。

问题出现在Fragment状态。碎片需要被调整:如果用户在单窗格模式下查看Detail Fragment B,然后旋转到双窗格模式,他们应该在左侧看到Fragment列表,在右侧看到Fragment列表。

移动碎片已经够糟糕的了,但后面的堆栈才是真正的问题。假设我处于双窗格模式,并选择Fragment C。Fragment列表显示在左侧,detail Fragment C显示在右侧。然后我旋转到单窗格模式。现在我应该看到Fragment C的细节,但如果我按下后退按钮,我应该回到Fragment列表。另一方面,如果我从单窗格模式开始,并建立一个后堆栈,然后切换到双窗格模式,应该没有后堆栈。

这似乎是任何人在尝试使用Fragments和/或对平板电脑友好时都会遇到的常见情况。当在单窗格和双窗格方向之间切换时,他们将不得不面对杂耍Fragments和操纵后堆栈的挑战。然而,到目前为止,我还没有找到答案或例子,只有许多基本的问题,如"我如何在平板电脑上并排显示两个片段"。

有人处理过这种情况吗?当用户从一个片段移动到另一个片段,来回旋转时,你的解决方案是什么?

我是这样做的:在纵向模式下,我为列表视图和详细信息视图创建了相同的容器视图,但列表视图的宽度为0dp。这样,当用户处于横向列表+细节模式时,如果他们向前导航,改变方向并按下后退按钮,列表和细节片段都将正确加载,但现在在纵向模式下,你只能看到细节视图。

我没有很复杂。如果你在纵向模式下导航,从列表到细节,然后旋转到横向模式,我会显示列表和细节,但按下后退键只会再次显示列表。我的理念是恢复后栈的原样,尽量减少多余或缺失的窗格,让芯片掉到该掉的地方。

但我认为你可以做得更好。让我们逐一分析一下。

用例1:用户处于横屏模式。用户导航到列表+详细信息窗格,并选择一个详细信息。如果用户旋转到纵向,他们应该只看到细节窗格。如果他们现在按回,而不是在列表+细节之前的UI,他们应该在导航回到他们开始的地方之前看到列表。

用例2:用户处于竖屏模式。用户导航到列表屏幕,然后是详细信息屏幕。当用户旋转到横向模式时,他们应该看到列表+细节。当他们按回时,他们应该跳过返回堆栈上的列表屏幕。

我花了一些时间在我的平板电脑上使用GMail应用程序,它似乎做得很好。

让我们从布局XML开始。竖屏和横屏版本都有两个窗格,竖屏版本只有一个:

layout/list_detail.xml
    <LinearLayout
        android:width="match_parent"
        android:height="match_parent"
        orientation="horizontal">
        <FrameLayout
            android:id="+id/list_pane"
            android:width="match_parent"
            android:height="match_parent"/>
        <FrameLayout
            android:id="+id/detail_pane"
            android:width="match_parent"
            android:height="match_parent"
            android:visibility="gone"/>
    </LinearLayout>
layout-landscape/list_detail.xml
    <LinearLayout
        android:width="match_parent"
        android:height="match_parent"
        orientation="horizontal">
        <FrameLayout
            android:id="+id/list_pane"
            android:width="240dp"
            android:height="match_parent"/>
        <FrameLayout
            android:id="+id/detail_pane"
            android:width="0dp"
            android:weight="1"
            android:height="match_parent"/>
    </LinearLayout>

这个想法是,片段在两个窗格将始终被加载,但在纵向模式下,你只能看到一个或另一个(通过切换哪个窗格是可见的,哪个窗格消失),在横向模式下,你看到两个。

我将假设在横向模式下,你最初加载带有列表上第一项的详细窗格。所以你总是有一个列表和一个细节,只是在竖屏中你现在看不到细节。

首先,我们想知道我们的UI是在列表模式还是细节模式,特别是当我们从横向切换到纵向时:

        private boolean mListMode;

onCreate()中,我们将初始化这个:

        mListMode = true;

onCreateView()中,我们想要恢复保存的模式状态(假设这里有片段):

        if (savedInstanceState != null) {
            mListMode = savedInstanceState.getBoolean("listMode");
        }
        if (getResources().getBoolean(R.bool.is_portrait)) {
            mListPane.setVisibility(mListMode ? View.VISIBLE : View.GONE);
            mDetailPane.setVisibility(mListMode ? View.GONE : View.VISIBLE);
        }

保存在

     @Override
     public void onSaveInstanceState(Bundle outState) {
         outState.putBoolean("listMode", mListMode);
         super.onSaveInstanceState(outState);
     }

现在列表项的点击逻辑将是这样的:

        if (mListMode) {
            if (getResources().getBoolean(R.bool.is_portrait)) {
                // TODO animate list -> detail
                mDetailPane.setVisibility(View.VISIBLE);
                mListPane.setVisibility(View.GONE);
            }
            mListMode = false;  // a click puts UI in detail mode, even in landscape
        }

你需要确保你设置了mListMode = false,以防用户点击细节窗格中的东西,在横向导航时向前导航,所以如果他们旋转到纵向并点击回来,他们会看到细节窗格。

好了,现在你需要一个技巧来处理纵向的后退按钮。你可以在你的UI中做这样的事情(让我们假设它是一个片段):

    public boolean onBackPressed() {
        if (getResources().getBoolean(R.bool.is_portrait) && ! mListMode) {
            // TODO animate detail -> list
            mDetailPane.setVisibility(View.GONE);
            mListPane.setVisibility(View.VISIBLE);
            mListMode = true;
            return true;   // we handled back button ourselves
        }
        return false;  // we didn't handle back button
    }

然后在你的活动中:

    @Override
    public void onBackPressed() {
        ListDetailFragment listDetailFragment = (ListDetailFragment ) getFragmentManager().findFragmentByTag(LIST_DETAIL_TAG);
        if (listDetailFragment != null && listDetailFragment.isResumed() && listDetailFragment.onBackPressed()) {
             return;
         }
         super.onBackPressed();
    }

我将把过渡动画留给读者作为练习。

@kris larson,我接受你的答案,以奖励你的辛勤工作和详细的解释!既然我一直在努力,并试图自己解决一些问题,我想我应该简要地分享一下我的一些尝试,这样其他人就可以从我的头痛中受益。我当然不会说这是最好的方法!

我制作了两个Fragment布局文件。其中一个包含一个ID为fragment_container_master的framayout和一个ID为fragment_container_detail的framayout。另一个只有fragment_container_detail framayout。我把双窗格的放在layout-w720dp中,把单窗格的放在layout中。

我注意到Android文档让它听起来像-sw("最小宽度")修饰符比-w("宽度")更可取。-sw取宽度或高度中较小者。我不认为这与这个用例相关:在宽屏幕上,我想要两个窗格,就这么简单。但也许有时使用-sw有很好的理由。

使用FragmentTransactions,我会将Fragment列表添加到fragment_container_master,并将默认的细节Fragment添加到fragment_container_detail。当用户点击列表中的不同片段时,我会替换fragment_container_detail中的片段。简单。

我将使用fragment_container_master的存在与否来知道我是在单窗格还是双窗格模式下操作。如果是单窗格,我一定要将片段过渡添加到后堆栈,并确保在fragment_container_detail中初始加载片段列表。

正如我所指出的,在单面板和双面板之间切换(比如旋转7英寸的平板电脑)是一个棘手的部分。我必须确保更新应用程序栏,以防止按钮被添加两次。

首先,我需要检测我是否在这两种模式之间切换。我可以在onCreate by中这样做1)检测我是在单屏还是双屏模式下操作2)检测是否有一个Fragment浮动与ID fragment_container_master如果我在单窗格模式,有一个主片段,它必须从双窗格模式留下。如果我在双窗格模式,没有主片段,它一定没有被初始化,因为我刚刚从单窗格模式切换。

如果我只是切换到双窗格模式,我需要加载主片段到fragment_container_master。如果细节片段是列表fragment,我删除它并将其添加到fragment_container_master。如果没有,我们已经显示了一个细节片段。你以为我可以把它留在那里…但是还有一个问题就是后面的堆栈。所以我保存Fragment状态,弹出它,用保存的状态重新创建它。然后我有一个双窗格的UI,后面的堆栈上没有任何东西,正如我想要的。

如果我只是切换到单窗格模式,我需要决定在详细视图中显示什么。如果带有ID fragment_container_detail的Fragment是默认的Fragment,我可以在那里显示Fragment列表(替换现有的)。如果它不是默认的Fragment,那么我仍然用列表Fragment替换它,但然后用之前存在的细节Fragment替换列表Fragment——确保将其添加到后堆栈中。

听起来很复杂,对吧?

情况越来越糟。

假设用户以双窗格模式拉起屏幕。用户与默认的细节片段交互,可能是滚动它或在字段中输入数据。然后屏幕切换为单屏模式。用户显然希望看到他们正在处理的细节片段。另一方面,假设用户打开屏幕,然后在做任何其他事情之前,更改为单窗格模式。他们不想看到默认的细节片段。他们希望看到列出所有其他片段的主片段。他们会很困惑,因为他们已经走上了一条他们没有选择的道路。试试Gmail应用程序,你就会知道它是如何工作的。在双面板模式下,切换到单面板模式(旋转平板电脑),你就会看到你的收件箱。但如果在双窗格模式下,你滚动第一封邮件或以某种方式与之交互,然后切换到单窗格模式,你就会看到那封邮件。我找不到任何聪明、简单的方法来做这件事。我不好意思这么说,但是当Fragment与Activity交互时,我在默认Fragment报告中有一堆不同的监听器。所以我必须为每个特定的情况编码:如果用户点击这个按钮或触摸这个ListView或做其他事情,然后报告用户与这个Fragment进行了交互。

我很抱歉没有详细说明或创建代码示例,只是想在我忘记之前摆脱我的头脑供将来参考。

最新更新