【问题标题】:Problems changing orientation from single-pane to double-pane Fragment layouts将方向从单窗格更改为双窗格 Fragment 布局的问题
【发布时间】:2016-09-29 18:16:16
【问题描述】:

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

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

问题出在 Fragment 状态。 Fragment 需要左右切换:如果用户在单窗格模式下查看 Detail Fragment B,然后旋转到双窗格模式,他们应该会看到左侧的列表 Fragment 和右侧的 Fragment B。

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

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

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

【问题讨论】:

    标签: android android-layout android-fragments


    【解决方案1】:

    这是我所做的:在纵向模式下,我为列表视图和详细视图创建了相同的容器视图,但列表视图的宽度为 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();
        }
    

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

    【讨论】:

    • 感谢您的详细解答!
    • 我意识到这只是为了示例目的,但为了阅读的启发,我会注意:最好使用基于大小的布局资源修饰符,例如 layout-w720dp,而不是布局-景观。这样它就会在宽屏幕上使用两个窗格,即使它是一个非常大的纵向模式设备或桌面窗口。
    • 好点。我的应用程序有一些血腥的代码来防止像手机这样的小屏幕设备进入横向模式,除了特定的 UI。猜猜这就是为什么没有想到的原因。
    【解决方案2】:

    @kris larson,我接受你的回答,以奖励你所有的辛勤工作和详细的解释!由于我努力前进并试图自己弄清楚一些事情,我想我会简明扼要地分享一些我尝试过的东西,以便其他人可以从我的头痛中受益。我当然不会说这是最好的方法!

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

    我会注意到,Android 文档听起来像是 -sw(“最小宽度”)修饰符比 -w(“宽度”)更可取。 -sw 将取较小的宽度或高度。我认为这与这个用例无关:在宽屏幕上,我想要两个窗格,就这么简单。但也许有时使用 -sw 是有充分理由的。

    使用 FragmentTransactions,我会将 Fragment 列表添加到 fragment_container_master 并将默认详细信息 Fragment 添加到 fragment_container_detail。当用户单击列表中的不同 Fragment 时,我会 .replace fragment_container_detail 中的 Fragment。很简单。

    我会使用 fragment_container_master 的存在与否来了解我是在单窗格还是双窗格模式下运行。如果是单窗格,我会确保将 Fragment 转换添加到返回堆栈,并确保最初在 fragment_container_detail 中加载片段列表。

    正如我所指出的,在单窗格和双窗格之间切换(例如在旋转 7" 平板电脑时)是棘手的部分。我必须确保更新应用栏以防止按钮被添加两次。

    首先我需要检测我是否在这两种模式之间切换。我可以在 onCreate 中做到这一点 1)检测我是在单窗格还是双窗格模式下操作 2) 检测是否有ID为fragment_container_master的Fragment浮动 如果我在单窗格模式下,并且有一个主 Fragment,它必须是从双窗格模式遗留下来的。 如果我在双窗格模式下,并且没有master Fragment,它肯定没有初始化,因为我刚刚从单窗格模式切换。

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

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

    听起来很复杂,对吧?

    情况会变得更糟。

    假设用户在双窗格模式下拉起屏幕。用户与默认的详细信息片段交互,可能滚动它或将数据输入到字段中。然后屏幕变为单窗格模式。用户显然希望查看他们正在使用的详细 Fragment。 另一方面,假设用户打开屏幕,然后在执行任何其他操作之前更改为单窗格模式。他们不想看到默认的细节片段。他们想查看列出所有其他 Fragment 的主 Fragment。如果他们已经走了一条他们没有选择的道路,他们会很困惑。 试试 Gmail 应用程序,您会看到它是如何工作的。在双窗格模式下,切换到单窗格模式(旋转平板电脑),您将看到您的收件箱。但是,如果在双窗格模式下您滚动第一封电子邮件或以某种方式与其交互,然后切换到单窗格模式,您将看到该电子邮件。 我找不到任何聪明、简单的方法来做到这一点。我很尴尬地说,但是当与 Fragment 进行交互时,我只是在默认的 Fragment 报告给 Activity 中有一堆不同的侦听器。所以我必须针对每种特定情况编写代码:如果用户单击此按钮或触摸此 ListView 或执行其他操作,则报告用户与此 Fragment 进行了交互。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-10-19
      • 2017-07-18
      • 1970-01-01
      • 1970-01-01
      • 2011-02-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多