Chad Schultz Chad Schultz - 1 month ago 33
Android Question

Problems changing orientation from single-pane to double-pane Fragment layouts

I have an Activity that contains multiple Fragments. On a tablet (720dp or wider) I would show a list in a Fragment on the left, and then one of multiple Fragments in a pane on the right. When the user selects different items from the list, the Fragment in the right pane changes. In single-pane mode, the user would see the list of Fragments, tap one and then the list Fragment would be swapped out for the detail Fragment.

Here's the tricky part: take a 7-inch tablet, so landscape is wide enough for two panes, but portrait is wide enough for just one. Rotate between single-pane mode and double-pane mode. Some things, like action bar icons and title, will need to be updated, but that's not too complicated.

The problem comes in the Fragment states. Fragments need to be juggled around: if the user was viewing Detail Fragment B in single-pane mode, then rotates to double-pane mode, they should see the list Fragment on the left and Fragment B on the right.

Moving Fragments around is bad enough, but the back stack is the real issue. Let's say I'm in double-pane mode and tab Fragment C. The list Fragment shows on the left, detail Fragment C on the right. Then I rotate to single-pane mode. Now I should see detail Fragment C, but if I press the back button I should go back to the list Fragment. On the other hand, if I start in single-pane mode and build up a back stack, then switch to double-pane mode, there should be no back stack.

This seems like a common situation anyone would run into when trying to use Fragments and/or be tablet-friendly. They would have to face with the challenges of juggling Fragments around and manipulating the back stack when switching between single-pane and double-pane orientations. Yet so far I have not found answers or examples of this, just many questions on basics like "how do I show two fragments side by side on a tablet".

Anyone dealt with this situation? What was your solution to present a consistent experience to the user as they moved from Fragment to Fragment, rotating back and forth?

Answer

Here's what I did: In portrait mode, I made the same container views for both the list view and the detail view, but the list view had a width of 0dp. This way, when the user was in landscape list+detail mode, if they navigated ahead, changed orientations and pressed the back button, both list and detail fragments would load correctly but now in portrait mode you would only see the detail view.

I didn't get very sophisticated with it. If you navigated through in portrait mode, going list to detail, then rotated to landscape, I would display list and detail but pressing the back button would just show the list again. My philosophy was to restore the back stack as it was and as far as redundant or missing panes, let the chips fall where they may.

But I think you can do better than that. Let's take each case and work it through.

Use Case 1: User is in landscape mode. User navigates to list+detail panes, and selects a detail. If the user rotates to portrait, they should see just the detail pane. If they now press back, instead of the UI before list+detail, they should see just the list before navigating back to where they started.

Use Case 2: User is in portrait mode. User navigates to list screen, then detail screen. When user rotates to landscape mode they should see list+detail. When they press back, they should skip over the list screen on the back stack.

I spent some quality time with the GMail app on my tablet, and it seems to do this nicely.

Let's start with the layout XML. Both portrait and landscape versions are going to have both panes, but in portrait you only see one:

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>

The idea is that fragments in both panes will always be loaded, but in portrait mode you only see one or the other (by switching which pane is visible and which pane is gone) and in landscape mode you see both.

I'm going to assume that in landscape mode you initially load the detail pane with the first item on the list. So you always have a list and a detail, just that in portrait you don't see the detail right now.

First, we want to know if our UI is in list mode or detail mode, especially when we go from landscape to portrait:

        private boolean mListMode;

In onCreate(), we'll initialize this:

        mListMode = true;

In onCreateView(), we want to recover the saved mode state (assuming a fragment here):

        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);
        }

which we have saved in

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

Now the list item click logic will go like this:

        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
        }

You'll need to make sure you set mListMode = false in case the user clicks on something in the detail pane that navigates forward while in landscape, so if they rotate to portrait and hit back, they'll see the detail pane.

Okay, so now you need a trick to handle the back button for portrait. You could do something like this in your UI (let's assume it's a fragment):

    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
    }

Then in your activity:

    @Override
    public void onBackPressed() {

        ListDetailFragment listDetailFragment = (ListDetailFragment ) getFragmentManager().findFragmentByTag(LIST_DETAIL_TAG);

        if (listDetailFragment != null && listDetailFragment.isResumed() && listDetailFragment.onBackPressed()) {
             return;
         }

         super.onBackPressed();
    }

I'll leave the transition animations as an exercise for the reader.

Comments